diff --git a/.changeset/move-proofkit-cli-home.md b/.changeset/move-proofkit-cli-home.md deleted file mode 100644 index 0f52902c..00000000 --- a/.changeset/move-proofkit-cli-home.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@proofkit/cli": patch -"create-proofkit": patch ---- - -Move current ProofKit CLI and create-proofkit packages back into public release flow. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f91c46ab..6a09c70d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -110,29 +110,6 @@ jobs: - name: Build run: pnpm ci:build - cli-smoke: - name: CLI External Integration Smoke Tests - if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - steps: - - name: Checkout Repo - uses: actions/checkout@v6 - - - name: Enable Corepack - run: corepack enable - - - name: Setup Node.js 22.x - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: "pnpm" - - - name: Install Dependencies - run: pnpm install --frozen-lockfile - - - name: Run CLI External Integration Smoke Tests - run: pnpm ci:cli-smoke - fmodata-e2e: name: fmodata E2E Tests if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' diff --git a/apps/docs/content/docs/webviewer/package.mdx b/apps/docs/content/docs/webviewer/package.mdx index 3f8723a5..68602fad 100644 --- a/apps/docs/content/docs/webviewer/package.mdx +++ b/apps/docs/content/docs/webviewer/package.mdx @@ -30,7 +30,7 @@ For web-based applications where you're looking to interact with the Data API us The [ProofKit AI workflow](/docs/ai/build-a-webviewer-app) can scaffold a full Web Viewer project and install the FileMaker add-on that provides the necessary layouts, scripts, and custom functions. Use that path when you want ProofKit to create the project structure for you. -If you already have a ProofKit Web Viewer project and need to install or update the FileMaker add-on manually, run `proofkit add addon webviewer` from the project root. That downloads the latest add-on from the ProofKit CDN and opens it in FileMaker; you still need to add the add-on into your FileMaker file. +When you scaffold a Web Viewer project with `proofkit init`, ProofKit downloads the latest FileMaker add-on from the ProofKit CDN and opens it in FileMaker automatically; you still need to add the add-on into your FileMaker file. For manual installation, follow the steps below. @@ -38,9 +38,8 @@ If you already have a ProofKit Web Viewer project and need to install or update {" "} This demo file is a very simplified example. To see more features, use the - [ProofKit AI workflow](/docs/ai/build-a-webviewer-app) to build a new app or run - `proofkit add addon webviewer` in an existing ProofKit project, then install - the FileMaker add-on. + [ProofKit AI workflow](/docs/ai/build-a-webviewer-app) to build a new app or + scaffold one with `proofkit init`, then install the FileMaker add-on. Use your preferred package manager to install the package. diff --git a/knip.config.ts b/knip.config.ts index b316b614..a073e39d 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -1,7 +1,6 @@ import type { KnipConfig } from "knip"; const config: KnipConfig = { - ignore: ["packages/cli/template/**"], ignoreBinaries: ["op", "vercel"], }; diff --git a/package.json b/package.json index 15223cd2..2eee691b 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,10 @@ "build": "turbo run build --filter={./packages/*} --filter=@proofkit/docs", "ci": "pnpm ci:pr", "ci:build": "pnpm build", - "ci:cli-smoke": "pnpm --filter @proofkit/cli build && pnpm exec varlock run -- pnpm --filter @proofkit/cli test:smoke", "ci:fmodata-e2e": "pnpm --filter @proofkit/fmodata test:e2e", "ci:lint": "pnpm lint && pnpm skill:check-versions", "ci:pr": "pnpm ci:lint && pnpm ci:typecheck && pnpm ci:test && pnpm ci:build", - "ci:release": "pnpm ci:lint && pnpm ci:typecheck && pnpm ci:test && pnpm ci:cli-smoke && pnpm ci:fmodata-e2e", + "ci:release": "pnpm ci:lint && pnpm ci:typecheck && pnpm ci:test && pnpm ci:fmodata-e2e", "ci:test": "pnpm test", "ci:typecheck": "pnpm typecheck", "dev": "turbo run dev", diff --git a/packages/cli-old/.yarnrc.yml b/packages/cli-old/.yarnrc.yml deleted file mode 100644 index c2e3ce63..00000000 --- a/packages/cli-old/.yarnrc.yml +++ /dev/null @@ -1,5 +0,0 @@ -packageExtensions: - chalk@5.0.1: - dependencies: - "#ansi-styles": npm:ansi-styles@6.1.0 - "#supports-color": npm:supports-color@9.2.2 diff --git a/packages/cli-old/CHANGELOG.md b/packages/cli-old/CHANGELOG.md deleted file mode 100644 index 535c5823..00000000 --- a/packages/cli-old/CHANGELOG.md +++ /dev/null @@ -1,285 +0,0 @@ -# @proofgeist/kit - -## 2.0.0-beta.22 - -### Minor Changes - -- 5544f68: - cli: Revamp the Web Viewer Vite template and harden `proofkit init` (ignore hidden files, improve non-interactive prompts, stop generating Cursor rules). - - cli: Install typegen skills locally when scaffolding projects. - - typegen: Add optional `fmHttp` config for using an FM HTTP proxy during metadata fetching. - - fmdapi/fmodata/webviewer: Add initial Codex skills for client and integration workflows. - -### Patch Changes - -- Updated dependencies [5544f68] -- Updated dependencies [f3980b1] -- Updated dependencies [8ca7a1e] -- Updated dependencies [1d4b69d] - - @proofkit/typegen@1.1.0-beta.17 - - @proofkit/fmdapi@5.1.0-beta.2 - -## 2.0.0-beta.21 - -### Patch Changes - -- Updated dependencies [2df365d] - - @proofkit/typegen@1.1.0-beta.16 - -## 2.0.0-beta.20 - -### Patch Changes - -- @proofkit/typegen@1.1.0-beta.15 - -## 2.0.0-beta.19 - -### Patch Changes - -- Updated dependencies [4e048d1] - - @proofkit/typegen@1.1.0-beta.14 - -## 2.0.0-beta.18 - -### Patch Changes - -- Updated dependencies [4928637] - - @proofkit/typegen@1.1.0-beta.13 - -## 2.0.0-beta.17 - -### Patch Changes - -- @proofkit/typegen@1.1.0-beta.12 - -## 2.0.0-beta.16 - -### Patch Changes - -- @proofkit/typegen@1.1.0-beta.11 - -## 2.0.0-beta.15 - -### Patch Changes - -- @proofkit/typegen@1.1.0-beta.10 - -## 2.0.0-beta.14 - -### Patch Changes - -- Updated dependencies [eb7d751] - - @proofkit/typegen@1.1.0-beta.9 - -## 2.0.0-beta.13 - -### Patch Changes - -- @proofkit/typegen@1.1.0-beta.8 - -## 2.0.0-beta.12 - -### Patch Changes - -- Updated dependencies [3b55d14] - - @proofkit/typegen@1.1.0-beta.7 - -## 2.0.0-beta.11 - -### Patch Changes - -- Updated dependencies - - @proofkit/typegen@1.1.0-beta.6 - -## 2.0.0-beta.10 - -### Patch Changes - -- Updated dependencies [ae07372] -- Updated dependencies [23639ec] -- Updated dependencies [dfe52a7] - - @proofkit/typegen@1.1.0-beta.5 - -## 2.0.0-beta.9 - -### Patch Changes - -- 863e1e8: Update tooling to Biome -- Updated dependencies [7dbfd63] -- Updated dependencies [863e1e8] - - @proofkit/typegen@1.1.0-beta.4 - - @proofkit/fmdapi@5.0.3-beta.1 - -## 2.0.0-beta.8 - -### Patch Changes - -- @proofkit/typegen@1.1.0-beta.3 - -## 2.0.0-beta.4 - -### Patch Changes - -- Updated dependencies [4d9d0e9] - - @proofkit/typegen@1.0.11-beta.1 - -## 1.1.8 - -### Patch Changes - -- 00177bf: Guard page add/remove against missing `src/app/navigation.tsx` so Web Viewer apps donโ€™t error when updating navigation. This safely no-ops when the navigation file isnโ€™t present. -- Updated dependencies [7c602a9] -- Updated dependencies [a29ca94] - - @proofkit/typegen@1.0.10 - - @proofkit/fmdapi@5.0.2 - -## 1.1.5 - -### Patch Changes - -- Run typegen code directly instead of via execa -- error trap around formatting -- Remove shared-utils dep - -## 1.1.0 - -### Minor Changes - -- 7429a1e: Add simultaneous support for Shadcn. New projects will have Shadcn initialized automatically, and the upgrade command will offer to automatically add support for Shadcn to an existing ProofKit project. - -### Patch Changes - -- b483d67: Update formatting after typegen to be more consistent -- f0ddde2: Upgrade next-safe-action to v8 (and related dependencies) -- 7c87649: Fix getFieldNamesForSchema function - -## 1.0.0 - -### Major Changes - -- c348e37: Support @proofkit namespaced packages - -### Patch Changes - -- Updated dependencies [16fb8bd] -- Updated dependencies [16fb8bd] -- Updated dependencies [16fb8bd] - - @proofkit/fmdapi@5.0.0 - -## 0.3.2 - -### Patch Changes - -- 8986819: Fix: name argument in add command optional -- 47aad62: Make the auth installer spinner good - -## 0.3.1 - -### Patch Changes - -- 467d0f9: Add new menu command to expose all proofkit functions more easily -- 6da944a: Ensure using authedActionClient in existing actions after adding auth -- b211fbd: Deploy command: run build on Vercel instead of locally. Use flag --local-build to build locally like before -- 39648a9: Fix: Webviewer addon installation flow -- d0627b2: update base package versions - -## 0.3.0 - -### Minor Changes - -- 846ae9a: Add new upgrade command to upgrade ProofKit components in an existing project. To start, this command only adds/updates the cursor rules in your project. - -### Patch Changes - -- e07341a: Always use accessorFn for tables for better type errors - -## 0.2.3 - -### Patch Changes - -- 217eb5b: Fixed infinite table queries for other field names -- 217eb5b: New infinite table editable template - -## 0.2.2 - -### Patch Changes - -- ffae753: Better https parsing when prompting for the FileMaker Server URL -- 415be19: Add options for password strength in fm-addon auth. Default to not check for compromised passwords -- af5feba: Fix the launch-fm script for web viewer - -## 0.2.1 - -### Patch Changes - -- 6e44193: update helper text for npm after adding page -- 6e44193: additional supression of hydration warning -- 6e44193: move question about adding data source for new project -- 183988b: fix import path for reset password helper -- 6e44193: Make an initial commit when initializing git repo -- e0682aa: Copy cursor rules.mdc file into the base project. - -## 0.2.0 - -### Minor Changes - -- 6073cfe: Allow deploying a demo file to your server instead of having to pick an existing file - -### Patch Changes - -- d0f5c6e: Fix: post-install template functions not running - -## 0.1.2 - -### Patch Changes - -- 92cb423: fix: runtime error due to external shared package - -## 0.1.1 - -### Patch Changes - -- f88583c: prompt user to login to Vercel if needed during deploy command - -## 0.1.0 - -### Minor Changes - -- c019363: Add Deploy command for Vercel - -### Patch Changes - -- 0b7bf78: Allow setup without any data sources - -## 0.0.15 - -### Patch Changes - -- 1ff4aa7: Hide options for unsupported features in webviewer apps -- 5cfd0aa: Add infinite table page template -- 063859a: Added Template: Editable Table -- de0c2ab: update shebang in index -- b7ad0cf: Stream output from the typegen command - -## 0.0.6 - -### Patch Changes - -- Adding pages - -## 0.0.3 - -### Patch Changes - -- add typegen command for fm - -## 0.0.2 - -### Patch Changes - -- fix auth in init - -## 0.0.2-beta.0 - -### Patch Changes - -- fix auth in init diff --git a/packages/cli-old/README.md b/packages/cli-old/README.md deleted file mode 100644 index f8e1efec..00000000 --- a/packages/cli-old/README.md +++ /dev/null @@ -1,19 +0,0 @@ -

- - Logo for ProofKit - -

- -

- ProofKit CLI -

- -

- Interactive CLI to manage your TypeScript projects that connect with FileMaker -

- -

- Get started with a new ProofKit project by running pnpm create proofkit -

- -View full documentation at [proofkit.proof.sh](https://proofkit.proof.sh) diff --git a/packages/cli-old/index.d.ts b/packages/cli-old/index.d.ts deleted file mode 100644 index 61865039..00000000 --- a/packages/cli-old/index.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface RouteLink { - label: string; - type: "link"; - href: string; - icon?: React.ReactNode; - /** If true, the route will only be considered active if the path is exactly this value. */ - exactMatch?: boolean; -} - -export interface RouteFunction { - label: string; - type: "function"; - icon?: React.ReactNode; - onClick: () => void; - /** If true, the route will only be considered active if the path is exactly this value. */ - exactMatch?: boolean; -} - -export type ProofKitRoute = RouteLink | RouteFunction; diff --git a/packages/cli-old/package.json b/packages/cli-old/package.json deleted file mode 100644 index 468a8075..00000000 --- a/packages/cli-old/package.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "name": "@proofkit/cli-old", - "version": "2.0.0-beta.22", - "private": true, - "description": "Create web application with the ProofKit stack", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/proofsh/proofkit.git", - "directory": "packages/cli-old" - }, - "keywords": [ - "proofkit", - "filemaker", - "ottomatic", - "proofgeist", - "proofsh", - "next.js", - "typescript" - ], - "type": "module", - "exports": { - ".": { - "types": "./index.d.ts", - "import": "./dist/index.js" - } - }, - "files": [ - "dist", - "template", - "README.md", - "index.d.ts", - "LICENSE", - "CHANGELOG.md", - "package.json" - ], - "engines": { - "node": "^22.0.0 || ^24.0.0 || ^26.0.0" - }, - "scripts": { - "typecheck": "tsc", - "build": "NODE_ENV=production tsdown && publint --strict", - "prepublishOnly": "pnpm build", - "dev": "tsdown --watch", - "clean": "rm -rf dist .turbo node_modules", - "start": "node dist/index.js", - "lint": "biome check . --write", - "lint:summary": "biome check . --reporter=summary", - "release": "changeset version", - "test": "pnpm test:contract", - "test:contract": "vitest run --config vitest.config.ts", - "test:smoke": "vitest run --config vitest.smoke.config.ts" - }, - "dependencies": { - "@better-fetch/fetch": "1.1.17", - "@clack/core": "^0.3.5", - "@clack/prompts": "^0.11.0", - "@inquirer/prompts": "^8.3.2", - "@proofkit/fmdapi": "workspace:*", - "@proofkit/typegen": "workspace:*", - "@types/glob": "^8.1.0", - "axios": "^1.13.2", - "chalk": "5.4.1", - "commander": "^14.0.2", - "dotenv": "^16.6.1", - "es-toolkit": "^1.43.0", - "execa": "^9.6.1", - "fast-glob": "^3.3.3", - "fs-extra": "^11.3.3", - "glob": "^11.1.0", - "gradient-string": "^2.0.2", - "handlebars": "^4.7.8", - "jiti": "^1.21.7", - "jsonc-parser": "^3.3.1", - "open": "^10.2.0", - "ora": "6.3.1", - "randomstring": "^1.3.1", - "semver": "^7.7.3", - "shadcn": "^2.10.0", - "sort-package-json": "^2.15.1", - "ts-morph": "^26.0.0" - }, - "devDependencies": { - "@auth/drizzle-adapter": "^1.11.1", - "@auth/prisma-adapter": "^1.6.0", - "@biomejs/biome": "2.3.11", - "@libsql/client": "^0.6.2", - "@planetscale/database": "^1.19.0", - "@prisma/adapter-planetscale": "^5.22.0", - "@prisma/client": "^5.22.0", - "@proofkit/registry": "workspace:*", - "@rollup/plugin-replace": "^6.0.3", - "@t3-oss/env-nextjs": "^0.10.1", - "@tanstack/react-query": "^5.90.16", - "@trpc/client": "11.0.0-rc.441", - "@trpc/next": "11.0.0-rc.441", - "@trpc/react-query": "11.0.0-rc.441", - "@trpc/server": "11.0.0-rc.441", - "@types/axios": "^0.14.4", - "@types/fs-extra": "^11.0.4", - "@types/gradient-string": "^1.1.6", - "@types/node": "^22.19.5", - "@types/randomstring": "^1.3.0", - "@types/react": "19.2.7", - "@types/semver": "^7.7.1", - "@vitest/coverage-v8": "^2.1.9", - "drizzle-kit": "^0.21.4", - "drizzle-orm": "^0.30.10", - "mysql2": "^3.16.0", - "next": "16.1.1", - "next-auth": "^4.24.13", - "postgres": "^3.4.8", - "prisma": "^5.22.0", - "publint": "^0.3.16", - "react": "19.2.3", - "react-dom": "19.2.3", - "superjson": "^2.2.6", - "tailwindcss": "^4.1.18", - "tsdown": "^0.14.2", - "type-fest": "^3.13.1", - "typescript": "^5.9.3", - "ultracite": "7.0.8", - "vitest": "^4.0.17", - "zod": "^4.3.5" - }, - "publishConfig": { - "access": "restricted" - } -} diff --git a/packages/cli-old/proofkit-cli-1.1.8.tgz b/packages/cli-old/proofkit-cli-1.1.8.tgz deleted file mode 100644 index 45e19d04..00000000 Binary files a/packages/cli-old/proofkit-cli-1.1.8.tgz and /dev/null differ diff --git a/packages/cli-old/src/cli/add/auth.ts b/packages/cli-old/src/cli/add/auth.ts deleted file mode 100644 index cab277f9..00000000 --- a/packages/cli-old/src/cli/add/auth.ts +++ /dev/null @@ -1,110 +0,0 @@ -import chalk from "chalk"; -import { Command } from "commander"; -import { z } from "zod/v4"; -import { cancel, select } from "~/cli/prompts.js"; - -import { addAuth } from "~/generators/auth.js"; -import { ciOption, debugOption, nonInteractiveOption } from "~/globalOptions.js"; -import { initProgramState, isNonInteractiveMode, state } from "~/state.js"; -import { getSettings } from "~/utils/parseSettings.js"; -import { abortIfCancel } from "../utils.js"; - -export async function runAddAuthAction() { - const settings = getSettings(); - if (settings.appType !== "browser") { - return cancel("Auth is not supported for your app type."); - } - if (settings.ui === "shadcn") { - return cancel("Adding auth is not yet supported for shadcn-based projects."); - } - - const authType = - state.authType ?? - abortIfCancel( - await select({ - message: "What auth provider do you want to use?", - options: [ - { - value: "fmaddon", - label: "FM Add-on Auth", - hint: "Self-hosted auth with email/password", - }, - { - value: "clerk", - label: "Clerk", - hint: "Hosted auth service with many providers", - }, - ], - }), - ); - - const type = z.enum(["clerk", "fmaddon"]).parse(authType); - state.authType = type; - - if (type === "fmaddon") { - const emailProviderAnswer = - state.emailProvider ?? - (isNonInteractiveMode() ? "none" : undefined) ?? - abortIfCancel( - await select({ - message: `What email provider do you want to use?\n${chalk.dim( - "Used to send email verification codes. If you skip this, the codes will be displayed here in your terminal.", - )}`, - options: [ - { - label: "Resend", - value: "resend", - hint: "Great dev experience", - }, - { - label: "Plunk", - value: "plunk", - hint: "Cheapest for <20k emails/mo, self-hostable", - }, - { label: "Other / I'll do it myself later", value: "none" }, - ], - }), - ); - - const emailProvider = z.enum(["plunk", "resend", "none"]).parse(emailProviderAnswer); - - state.emailProvider = emailProvider; - - await addAuth({ - options: { - type, - emailProvider: emailProvider === "none" ? undefined : emailProvider, - }, - }); - } else { - await addAuth({ options: { type } }); - } -} - -export const makeAddAuthCommand = () => { - const addAuthCommand = new Command("auth") - .description("Add authentication to your project") - .option("--authType ", "Type of auth provider to use") - .option("--emailProvider ", "Email provider to use (only for FM Add-on Auth)") - .option("--apiKey ", "API key to use for the email provider (only for FM Add-on Auth)") - .addOption(ciOption) - .addOption(nonInteractiveOption) - .addOption(debugOption) - - .action(async () => { - const settings = getSettings(); - if (settings.ui === "shadcn") { - throw new Error("Shadcn projects should add auth using the template registry"); - } - if (settings.auth.type !== "none") { - throw new Error("Auth already exists"); - } - await runAddAuthAction(); - }); - - addAuthCommand.hook("preAction", (thisCommand) => { - initProgramState(thisCommand.opts()); - }); - - return addAuthCommand; -}; diff --git a/packages/cli-old/src/cli/add/data-source/deploy-demo-file.ts b/packages/cli-old/src/cli/add/data-source/deploy-demo-file.ts deleted file mode 100644 index 7d460058..00000000 --- a/packages/cli-old/src/cli/add/data-source/deploy-demo-file.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { createDataAPIKeyWithCredentials, getDeploymentStatus, startDeployment } from "~/cli/ottofms.js"; -import * as p from "~/cli/prompts.js"; - -export const filename = "ProofKitDemo.fmp12"; - -export async function deployDemoFile({ - url, - token, - operation, -}: { - url: URL; - token: string; - operation: "install" | "replace"; -}): Promise<{ apiKey: string }> { - const deploymentJSON = { - scheduled: false, - label: "Install ProofKit Demo", - deployments: [ - { - name: "Install ProofKit Demo", - source: { - type: "url", - url: "https://proofkit.proof.sh/proofkit-demo/manifest.json", - }, - fileOperations: [ - { - target: { - fileName: filename, - }, - operation, - source: { - fileName: "ProofKitDemo.fmp12", - }, - location: { - folder: "default", - subFolder: "", - }, - }, - ], - concurrency: 1, - options: { - closeFilesAfterBuild: false, - keepFilesClosedAfterComplete: false, - transferContainerData: false, - }, - }, - ], - abortRemaining: false, - }; - - const spinner = p.spinner(); - spinner.start("Deploying ProofKit Demo file..."); - - const { - response: { subDeploymentIds }, - } = await startDeployment({ - payload: deploymentJSON, - url, - token, - }); - - const deploymentId = subDeploymentIds[0]; - if (!deploymentId) { - throw new Error("No deployment ID returned from the server"); - } - - while (true) { - // wait 2.5 seconds, then poll the status again - await new Promise((resolve) => setTimeout(resolve, 2500)); - - const { - response: { status, running }, - } = await getDeploymentStatus({ - url, - token, - deploymentId, - }); - if (!running) { - if (status !== "complete") { - throw new Error("Deployment didn't complete"); - } - break; - } - } - - const { apiKey } = await createDataAPIKeyWithCredentials({ - filename, - username: "admin", - password: "admin", - url, - }); - - spinner.stop(); - - return { apiKey }; -} diff --git a/packages/cli-old/src/cli/add/data-source/filemaker.ts b/packages/cli-old/src/cli/add/data-source/filemaker.ts deleted file mode 100644 index e7c00218..00000000 --- a/packages/cli-old/src/cli/add/data-source/filemaker.ts +++ /dev/null @@ -1,441 +0,0 @@ -import chalk from "chalk"; -import { SemVer } from "semver"; -import type { z } from "zod/v4"; -import { createDataAPIKey, getOttoFMSToken, listAPIKeys, listFiles } from "~/cli/ottofms.js"; -import * as p from "~/cli/prompts.js"; -import { abortIfCancel } from "~/cli/utils.js"; -import { addLayout, addToFmschemaConfig, ensureWebviewerFmMcpConfig } from "~/generators/fmdapi.js"; -import { getFmMcpStatus } from "~/helpers/fmMcp.js"; -import { fetchServerVersions } from "~/helpers/version-fetcher.js"; -import { isNonInteractiveMode } from "~/state.js"; -import { addPackageDependency } from "~/utils/addPackageDependency.js"; -import { addToEnv } from "~/utils/addToEnvs.js"; -import { type dataSourceSchema, getSettings, setSettings } from "~/utils/parseSettings.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; -import { validateAppName } from "~/utils/validateAppName.js"; -import { runAddSchemaAction } from "../fmschema.js"; -import { deployDemoFile, filename } from "./deploy-demo-file.js"; - -export async function promptForFileMakerDataSource({ - projectDir, - ...opts -}: { - projectDir: string; - name?: string; - server?: string; - adminApiKey?: string; - fileName?: string; - dataApiKey?: string; - layoutName?: string; - schemaName?: string; -}) { - const settings = getSettings(); - - if (settings.appType === "webviewer") { - const fmMcpStatus = await getFmMcpStatus(); - const connectedFileName = fmMcpStatus.connectedFiles[0]; - const localDataSourceName = opts.name ?? "filemaker"; - - if (!opts.server && fmMcpStatus.healthy && connectedFileName) { - addPackageDependency({ - projectDir, - dependencies: ["@proofkit/fmdapi"], - devMode: false, - }); - - await ensureWebviewerFmMcpConfig({ - projectDir, - connectedFileName, - dataSourceName: localDataSourceName, - baseUrl: fmMcpStatus.baseUrl, - }); - - // Persist the datasource in project settings - const newDataSource: z.infer = { - type: "fm", - name: localDataSourceName, - envNames: - localDataSourceName === "filemaker" - ? { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - } - : { - database: `${localDataSourceName.toUpperCase()}_FM_DATABASE`, - server: `${localDataSourceName.toUpperCase()}_FM_SERVER`, - apiKey: `${localDataSourceName.toUpperCase()}_OTTO_API_KEY`, - }, - }; - settings.dataSources.push(newDataSource); - setSettings(settings); - - if (opts.layoutName && opts.schemaName) { - await addLayout({ - projectDir, - dataSourceName: localDataSourceName, - schemas: [ - { - layoutName: opts.layoutName, - schemaName: opts.schemaName, - valueLists: "allowEmpty", - }, - ], - }); - } else if (opts.layoutName || opts.schemaName) { - throw new Error("Both --layoutName and --schemaName must be provided together."); - } else { - p.note( - `Detected local FM MCP at ${fmMcpStatus.baseUrl} with connected file "${connectedFileName}". Edit ${chalk.cyan( - "proofkit-typegen.config.jsonc", - )} to add layouts, then run ${chalk.cyan("pnpm typegen")} or ${chalk.cyan("pnpm typegen:ui")}.`, - "Local FileMaker detected", - ); - } - - return; - } - - if (!opts.server && isNonInteractiveMode()) { - throw new Error( - "No local FM MCP connection was detected and no FileMaker server was provided. Start the local FM MCP proxy with a connected file or rerun with --server.", - ); - } - - if (!opts.server) { - const fallbackAction = abortIfCancel( - await p.select({ - message: - "Local FM MCP was not detected. Do you want to continue with hosted FileMaker server setup or skip for now?", - options: [ - { - label: "Continue with hosted setup", - value: "hosted", - }, - { - label: "Skip for now", - value: "skip", - }, - ], - }), - ); - - if (fallbackAction === "skip") { - p.note( - `You can come back later with ${chalk.cyan("proofkit add data")} after starting FM MCP locally or when you have a hosted server ready.`, - ); - return; - } - } - } - - const existingFmDataSourceNames = settings.dataSources.filter((ds) => ds.type === "fm").map((ds) => ds.name); - - const server = await getValidFileMakerServerUrl(opts.server); - - const canDoBrowserLogin = server.ottoVersion && server.ottoVersion.compare(new SemVer("4.7.0")) > 0; - - if (!(canDoBrowserLogin || opts.adminApiKey)) { - return p.cancel( - "OttoFMS 4.7.0 or later is required to auto-login with this CLI. Please install/upgrade OttoFMS on your server, or pass an Admin API key with the --adminApiKey flag then try again", - ); - } - - const token = opts.adminApiKey || (await getOttoFMSToken({ url: server.url })).token; - - const fileList = await listFiles({ url: server.url, token }); - const demoFileExists = fileList.map((f) => f.filename.replace(".fmp12", "")).includes(filename.replace(".fmp12", "")); - let fmFile = opts.fileName; - while (true) { - fmFile = - opts.fileName || - abortIfCancel( - await p.searchSelect({ - message: `Which file would you like to connect to? ${chalk.dim("(TIP: Select the file where your data is stored)")}`, - searchLabel: "Search files", - emptyMessage: "No matching files found.", - options: [ - { - value: "$deployDemoFile", - label: "Deploy NEW ProofKit Demo File", - hint: "Use OttoFMS to deploy a new file for testing", - keywords: ["demo", "proofkit"], - }, - ...fileList - .sort((a, b) => a.filename.localeCompare(b.filename)) - .map((file) => ({ - value: file.filename, - label: file.filename, - hint: file.status, - keywords: [file.filename], - })), - ], - }), - ); - - if (fmFile !== "$deployDemoFile") { - break; - } - - if (demoFileExists) { - const replace = abortIfCancel( - await p.confirm({ - message: "The demo file already exists, do you want to replace it with a fresh copy?", - active: "Yes, replace", - inactive: "No, select another file", - initialValue: false, - }), - ); - if (replace) { - break; - } - } else { - break; - } - } - - if (!fmFile) { - throw new Error("No file selected"); - } - - let dataApiKey = opts.dataApiKey; - if (fmFile === "$deployDemoFile") { - const { apiKey } = await deployDemoFile({ - url: server.url, - token, - operation: demoFileExists ? "replace" : "install", - }); - dataApiKey = apiKey; - fmFile = filename; - opts.layoutName = opts.layoutName ?? "API_Contacts"; - opts.schemaName = opts.schemaName ?? "Contacts"; - } else { - const allApiKeys = await listAPIKeys({ url: server.url, token }); - const thisFileApiKeys = allApiKeys.filter((key) => key.database === fmFile); - - if (!dataApiKey && thisFileApiKeys.length > 0) { - const selectedKey = abortIfCancel( - await p.searchSelect({ - message: `Which OttoFMS Data API key would you like to use? ${chalk.dim(`(This determines the access that you'll have to the data in this file)`)}`, - searchLabel: "Search API keys", - emptyMessage: "No matching API keys found.", - options: [ - ...thisFileApiKeys.map((key) => ({ - value: key.key, - label: `${chalk.bold(key.label)} - ${key.user}`, - hint: `${key.key.slice(0, 5)}...${key.key.slice(-4)}`, - keywords: [key.label, key.user, key.database], - })), - { - value: "create", - label: "Create a new API key", - hint: "Requires FileMaker credentials for this file", - keywords: ["create", "new"], - }, - ], - }), - ); - if (typeof selectedKey !== "string") { - throw new Error("Invalid key"); - } - if (selectedKey !== "create") { - dataApiKey = selectedKey; - } - } - - if (!dataApiKey) { - // data api was not provided, prompt to create a new one - const resp = await createDataAPIKey({ - filename: fmFile, - url: server.url, - }); - dataApiKey = resp.apiKey; - } - } - if (!dataApiKey) { - throw new Error("No API key"); - } - - const name = - existingFmDataSourceNames.length === 0 - ? "filemaker" - : (opts.name ?? - abortIfCancel( - await p.text({ - message: "What do you want to call this data source?", - validate: (value) => { - if (value === "filemaker") { - return "That name is reserved"; - } - - // require name to be unique - if (existingFmDataSourceNames?.includes(value)) { - return "That name is already in use in this project, pick something unique"; - } - - // require name to be alphanumeric, lowercase, etc - return validateAppName(value); - }, - }), - )); - - const newDataSource: z.infer = { - type: "fm", - name, - envNames: - name === "filemaker" - ? { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - } - : { - database: `${name.toUpperCase()}_FM_DATABASE`, - server: `${name.toUpperCase()}_FM_SERVER`, - apiKey: `${name.toUpperCase()}_OTTO_API_KEY`, - }, - }; - - const project = getNewProject(projectDir); - - const schemaFile = await addToEnv({ - projectDir, - project, - envs: [ - { - name: newDataSource.envNames.database, - zodValue: `z.string().endsWith(".fmp12")`, - defaultValue: fmFile, - type: "server", - }, - { - name: newDataSource.envNames.server, - zodValue: "z.string().url()", - type: "server", - defaultValue: server.url.origin, - }, - { - name: newDataSource.envNames.apiKey, - zodValue: `z.string().startsWith("dk_") as z.ZodType`, - type: "server", - defaultValue: dataApiKey, - }, - ], - }); - - const fmdapiImport = schemaFile.getImportDeclaration((imp) => imp.getModuleSpecifierValue() === "@proofkit/fmdapi"); - if (fmdapiImport) { - fmdapiImport - .getNamedImports() - .find((imp) => imp.getName() === "OttoAPIKey") - ?.remove(); - fmdapiImport.addNamedImport({ name: "OttoAPIKey", isTypeOnly: true }); - } else { - schemaFile.addImportDeclaration({ - namedImports: [{ name: "OttoAPIKey", isTypeOnly: true }], - moduleSpecifier: "@proofkit/fmdapi", - }); - } - - addPackageDependency({ - projectDir, - dependencies: ["@proofkit/fmdapi"], - devMode: false, - }); - - settings.dataSources.push(newDataSource); - setSettings(settings); - - addToFmschemaConfig({ - dataSourceName: name, - envNames: name === "filemaker" ? undefined : newDataSource.envNames, - }); - - await formatAndSaveSourceFiles(project); - - // now prompt for layout - await runAddSchemaAction({ - settings, - sourceName: name, - projectDir, - layoutName: opts.layoutName, - schemaName: opts.schemaName, - valueLists: "allowEmpty", - }); -} - -async function getValidFileMakerServerUrl(defaultServerUrl?: string | undefined): Promise<{ - url: URL; - fmsVersion: SemVer; - ottoVersion: SemVer | null; -}> { - const spinner = p.spinner(); - let url: URL | null = null; - let fmsVersion: SemVer | null = null; - let ottoVersion: SemVer | null = null; - let serverUrlToUse = defaultServerUrl; - - while (fmsVersion === null) { - const serverUrl = - serverUrlToUse ?? - abortIfCancel( - await p.text({ - message: `What is the URL of your FileMaker Server?\n${chalk.cyan("TIP: You can copy any valid path on the server and paste it here.")}`, - validate: (value) => { - try { - // try to make sure the url is https - let normalizedValue = value; - if (!normalizedValue.startsWith("https://")) { - if (normalizedValue.startsWith("http://")) { - normalizedValue = normalizedValue.replace("http://", "https://"); - } else { - normalizedValue = `https://${normalizedValue}`; - } - } - - // try to make sure the url is valid - new URL(normalizedValue); - return; - } catch { - return "Please enter a valid URL"; - } - }, - }), - ); - - try { - url = new URL(serverUrl); - } catch { - p.log.error(`Invalid URL: ${serverUrl.toString()}`); - continue; - } - - spinner.start("Validating Server URL..."); - - // check for FileMaker and Otto versions - const { fmsInfo, ottoInfo } = await fetchServerVersions({ - url: url.origin, - }); - - spinner.stop(); - - const fmsVersionString = fmsInfo.ServerVersion.split(" ")[0]; - if (!fmsVersionString) { - p.log.error("Unable to parse FileMaker Server version"); - serverUrlToUse = undefined; - continue; - } - fmsVersion = new SemVer(fmsVersionString); - ottoVersion = ottoInfo?.Otto.version ? new SemVer(ottoInfo.Otto.version) : null; - serverUrlToUse = undefined; - } - - if (url === null) { - throw new Error("Unable to get FileMaker Server URL"); - } - - p.note(`๐ŸŽ‰ FileMaker Server version ${fmsVersion} detected \n - ${ottoVersion ? `๐ŸŽ‰ OttoFMS version ${ottoVersion} detected` : "โŒ OttoFMS not detected"}`); - - return { url, ottoVersion, fmsVersion }; -} diff --git a/packages/cli-old/src/cli/add/data-source/index.ts b/packages/cli-old/src/cli/add/data-source/index.ts deleted file mode 100644 index 6d73789b..00000000 --- a/packages/cli-old/src/cli/add/data-source/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Command } from "commander"; -import { z } from "zod/v4"; -import * as p from "~/cli/prompts.js"; - -import { ensureProofKitProject } from "~/cli/utils.js"; -import { ciOption, nonInteractiveOption } from "~/globalOptions.js"; -import { initProgramState } from "~/state.js"; -import { promptForFileMakerDataSource } from "./filemaker.js"; - -const dataSourceType = z.enum(["fm", "supabase"]); -export const runAddDataSourceCommand = async () => { - const dataSource = dataSourceType.parse( - await p.select({ - message: "Which data souce do you want to add?", - options: [ - { label: "FileMaker", value: "fm" }, - { label: "Supabase", value: "supabase" }, - ], - }), - ); - - if (dataSource === "supabase") { - throw new Error("Not implemented"); - } - if (dataSource === "fm") { - await promptForFileMakerDataSource({ projectDir: process.cwd() }); - } else { - throw new Error("Invalid data source"); - } -}; - -export const makeAddDataSourceCommand = () => { - const addDataSourceCommand = new Command("data"); - addDataSourceCommand.description("Add a new data source to your project"); - addDataSourceCommand.addOption(ciOption); - addDataSourceCommand.addOption(nonInteractiveOption); - - addDataSourceCommand.hook("preAction", (_thisCommand, actionCommand) => { - initProgramState(actionCommand.opts()); - const settings = ensureProofKitProject({ commandName: "add" }); - actionCommand.setOptionValue("settings", settings); - }); - - // addDataSourceCommand.action(); - return addDataSourceCommand; -}; diff --git a/packages/cli-old/src/cli/add/fmschema.ts b/packages/cli-old/src/cli/add/fmschema.ts deleted file mode 100644 index b63dc427..00000000 --- a/packages/cli-old/src/cli/add/fmschema.ts +++ /dev/null @@ -1,216 +0,0 @@ -import path from "node:path"; -import type { OttoAPIKey } from "@proofkit/fmdapi"; -import type { ValueListsOptions } from "@proofkit/typegen/config"; -import chalk from "chalk"; -import { Command } from "commander"; -import dotenv from "dotenv"; -import { z } from "zod/v4"; -import * as p from "~/cli/prompts.js"; -import { addLayout, getExistingSchemas } from "~/generators/fmdapi.js"; -import { state } from "~/state.js"; -import { getSettings, type Settings } from "~/utils/parseSettings.js"; -import { commonFileMakerLayoutPrefixes, getLayouts } from "../fmdapi.js"; -import { abortIfCancel } from "../utils.js"; - -// Regex to validate JavaScript variable names -const VALID_JS_VARIABLE_NAME = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; - -export const runAddSchemaAction = async (opts?: { - projectDir?: string; - settings: Settings; - sourceName?: string; - layoutName?: string; - schemaName?: string; - valueLists?: ValueListsOptions; -}) => { - const settings = getSettings(); - const projectDir = state.projectDir; - let sourceName = opts?.sourceName; - if (sourceName) { - sourceName = opts?.sourceName; - } else if (settings.dataSources.filter((s) => s.type === "fm").length > 1) { - // if there is more than one fm data source, we need to prompt for which one to add the layout to - const dataSourceName = await p.select({ - message: "Which FileMaker data source do you want to add a layout to?", - options: settings.dataSources.filter((s) => s.type === "fm").map((s) => ({ label: s.name, value: s.name })), - }); - if (p.isCancel(dataSourceName)) { - p.cancel(); - process.exit(0); - } - sourceName = z.string().parse(dataSourceName); - } - - if (!sourceName) { - sourceName = "filemaker"; - } - - const dataSource = settings.dataSources.filter((s) => s.type === "fm").find((s) => s.name === sourceName); - if (!dataSource) { - throw new Error(`FileMaker data source ${sourceName} not found in your ProofKit config`); - } - - const spinner = p.spinner(); - spinner.start("Loading layouts from your FileMaker file..."); - if (settings.envFile) { - dotenv.config({ - path: path.join(projectDir, settings.envFile), - }); - } - - const dataApiKey = process.env[dataSource.envNames.apiKey]; - const fmFile = process.env[dataSource.envNames.database]; - const server = process.env[dataSource.envNames.server]; - - if (!(dataApiKey && fmFile && server)) { - spinner.stop("Failed to load layouts"); - p.cancel("Missing required environment variables. Please check your .env file."); - process.exit(1); - } - - // Validate API key format - if (!(dataApiKey.startsWith("KEY_") || dataApiKey.startsWith("dk_"))) { - spinner.stop("Failed to load layouts"); - p.cancel("Invalid API key format. API key must start with 'KEY_' or 'dk_'."); - process.exit(1); - } - - // Type assertion after validation - const validatedApiKey: OttoAPIKey = dataApiKey as OttoAPIKey; - - const layouts = await getLayouts({ - dataApiKey: validatedApiKey, - fmFile, - server, - }); - - const existingConfigResults = getExistingSchemas({ - projectDir, - dataSourceName: sourceName, - }); - - const existingLayouts = existingConfigResults.map((s) => s.layout).filter(Boolean); - - const existingSchemas = existingConfigResults.map((s) => s.schemaName).filter(Boolean); - - spinner.stop("Loaded layouts from your FileMaker file"); - - if (existingLayouts.length > 0) { - p.note(existingLayouts.join("\n"), "Detected existing layouts in your project"); - } - - // list other common layout names to exclude - existingLayouts.push("-"); - - let passedInLayoutName: string | undefined = opts?.layoutName; - if (passedInLayoutName === "" || !layouts.includes(passedInLayoutName ?? "")) { - passedInLayoutName = undefined; - } - - const selectedLayout = - passedInLayoutName ?? - abortIfCancel( - await p.searchSelect({ - message: "Select a new layout to read data from", - searchLabel: "Search layouts", - emptyMessage: "No matching layouts found.", - options: layouts - .filter((layout) => !existingLayouts.includes(layout)) - .map((layout) => ({ - label: layout, - value: layout, - keywords: [layout], - })), - }), - ); - - const defaultSchemaName = getDefaultSchemaName(selectedLayout); - const schemaName = - opts?.schemaName || - abortIfCancel( - await p.text({ - message: `Enter a friendly name for the new schema.\n${chalk.dim("This will the name by which you refer to this layout in your codebase")}`, - // initialValue: selectedLayout, - defaultValue: defaultSchemaName, - placeholder: defaultSchemaName, - validate: (input) => { - if (input === "") { - return; // allow empty input for the default value - } - // ensure the input is a valid JS variable name - if (!VALID_JS_VARIABLE_NAME.test(input)) { - return "Name must consist of only alphanumeric characters, '_', and must not start with a number"; - } - if (existingSchemas.includes(input)) { - return "Schema name must be unique"; - } - return; - }, - }), - ).toString(); - - const valueLists = - opts?.valueLists ?? - ((await p.select({ - message: `Should we use value lists on this layout?\n${chalk.dim( - "This will allow fields that contain a value list to be auto-completed in typescript and also validated to prevent incorrect values", - )}`, - options: [ - { - label: "Yes, but allow empty fields", - value: "allowEmpty", - hint: "Empty fields or values that don't match the value list will be converted to an empty string", - }, - { - label: "Yes; empty values should fail validation", - value: "strict", - hint: "Empty fields or values that don't match the value list will cause validation to fail", - }, - { - label: "No, ignore value lists", - value: "ignore", - hint: "Fields will just be typed as strings", - }, - ], - })) as ValueListsOptions); - - const valueListsValidated = z.enum(["ignore", "allowEmpty", "strict"]).catch("ignore").parse(valueLists); - - await addLayout({ - runCodegen: true, - projectDir, - dataSourceName: sourceName, - schemas: [ - { - layoutName: selectedLayout, - schemaName, - valueLists: valueListsValidated, - }, - ], - }); - - p.outro(`Layout "${selectedLayout}" added to your project as "${schemaName}"`); -}; - -export const makeAddSchemaCommand = () => { - const addSchemaCommand = new Command("layout") - .alias("schema") - .description("Add a new layout to your fmschema file") - .action(async (opts: { settings: Settings }) => { - const settings = opts.settings; - - await runAddSchemaAction({ settings }); - }); - - return addSchemaCommand; -}; - -function getDefaultSchemaName(layout: string) { - let schemaName = layout.replace(/[-\s]/g, "_"); - for (const prefix of commonFileMakerLayoutPrefixes) { - if (schemaName.startsWith(prefix)) { - schemaName = schemaName.replace(prefix, ""); - } - } - return schemaName; -} diff --git a/packages/cli-old/src/cli/add/index.ts b/packages/cli-old/src/cli/add/index.ts deleted file mode 100644 index b3e0d543..00000000 --- a/packages/cli-old/src/cli/add/index.ts +++ /dev/null @@ -1,190 +0,0 @@ -import type { RegistryIndex } from "@proofkit/registry"; -import { Command } from "commander"; -import { capitalize, groupBy, uniq } from "es-toolkit"; -import ora from "ora"; -import { select } from "~/cli/prompts.js"; -import { ciOption, debugOption, nonInteractiveOption } from "~/globalOptions.js"; -import { initProgramState, state } from "~/state.js"; -import { logger } from "~/utils/logger.js"; -import { getSettings, type Settings } from "~/utils/parseSettings.js"; -import { runAddReactEmailCommand } from "../react-email.js"; -import { runAddTanstackQueryCommand } from "../tanstack-query.js"; -import { abortIfCancel, ensureProofKitProject } from "../utils.js"; -import { makeAddAuthCommand, runAddAuthAction } from "./auth.js"; -import { makeAddDataSourceCommand, runAddDataSourceCommand } from "./data-source/index.js"; -import { makeAddSchemaCommand, runAddSchemaAction } from "./fmschema.js"; -import { makeAddPageCommand, runAddPageAction } from "./page/index.js"; -import { installFromRegistry } from "./registry/install.js"; -import { listItems } from "./registry/listItems.js"; -import { preflightAddCommand } from "./registry/preflight.js"; - -const runAddFromRegistry = async (_options?: { noInstall?: boolean }) => { - const settings = getSettings(); - - const spinner = ora("Loading available components...").start(); - let items: RegistryIndex; - try { - items = await listItems(); - } catch (error) { - spinner.fail("Failed to load registry components"); - logger.error(error); - return; - } - - const itemsNotInstalled = items.filter((item) => !settings.registryTemplates.includes(item.name)); - - const groupedByCategory = groupBy(itemsNotInstalled, (item) => item.category); - const categories = uniq(itemsNotInstalled.map((item) => item.category)); - - spinner.succeed(); - - const addType = abortIfCancel( - await select({ - message: "What do you want to add to your project?", - options: [ - // if there are pages available to install, show them first - ...(categories.includes("page") ? [{ label: "Page", value: "page" }] : []), - - // only show schema option if there is at least one data source - ...(settings.dataSources.length > 0 - ? [ - { - label: "Schema", - value: "schema", - hint: "load data from a new table or layout from an existing data source", - }, - ] - : []), - - { - label: "Data Source", - value: "data", - hint: "to connect to a new database or FileMaker file", - }, - - // show the rest of the categories - ...categories - .filter((category) => category !== "page") - .map((category) => ({ - label: capitalize(category), - value: category, - })), - ], - }), - ); - - if (addType === "schema") { - await runAddSchemaAction(); - } else if (addType === "data") { - await runAddDataSourceCommand(); - } else if ((categories as string[]).includes(addType)) { - // one of the categories - const itemsFromCategory = groupedByCategory[addType as keyof typeof groupedByCategory]; - - const itemName = abortIfCancel( - await select({ - message: `Select a ${addType} to add to your project`, - options: itemsFromCategory.map((item) => ({ - label: item.title, - hint: item.description, - value: item.name, - })), - }), - ); - - await installFromRegistry(itemName); - } else { - logger.error(`Could not find any available components in the category "${addType}"`); - } -}; - -export const runAdd = async (name: string | undefined, options?: { noInstall?: boolean }) => { - if (name === "tanstack-query") { - return await runAddTanstackQueryCommand(); - } - if (name !== undefined) { - // an arbitrary name was provided, so we'll try to install from the registry - return await installFromRegistry(name); - } - - let settings: Settings; - try { - settings = getSettings(); - } catch { - await preflightAddCommand(); - return await runAddFromRegistry(options); - } - - if (settings.ui === "shadcn") { - return await runAddFromRegistry(options); - } - ensureProofKitProject({ commandName: "add" }); - - const addType = abortIfCancel( - await select({ - message: "What do you want to add to your project?", - options: [ - { label: "Page", value: "page" }, - // only show schema option if there is at least one data source - ...(settings.dataSources.length > 0 - ? [ - { - label: "Schema", - value: "schema", - hint: "load data from a new table or layout from an existing data source", - }, - ] - : []), - { label: "React Email", value: "react-email" }, - { - label: "Data Source", - value: "data", - hint: "to connect to a new database or FileMaker file", - }, - ...(settings.auth.type === "none" && settings.appType === "browser" ? [{ label: "Auth", value: "auth" }] : []), - ], - }), - ); - - if (addType === "auth") { - await runAddAuthAction(); - } else if (addType === "data") { - await runAddDataSourceCommand(); - } else if (addType === "page") { - await runAddPageAction(); - } else if (addType === "schema") { - await runAddSchemaAction(); - } else if (addType === "react-email") { - await runAddReactEmailCommand({ noInstall: options?.noInstall }); - } -}; - -export const makeAddCommand = () => { - const addCommand = new Command("add") - .description("Add a new component to your project") - .argument("[name]", "Type of component to add") - .addOption(ciOption) - .addOption(nonInteractiveOption) - .addOption(debugOption) - .option("--noInstall", "Do not run your package manager install command", false) - .action(async (name, options) => { - await runAdd(name, options); - }); - - addCommand.hook("preAction", (_thisCommand, _actionCommand) => { - // console.log("preAction", _actionCommand.opts()); - initProgramState(_actionCommand.opts()); - state.baseCommand = "add"; - }); - addCommand.hook("preSubcommand", (_thisCommand, _subCommand) => { - // console.log("preSubcommand", _subCommand.opts()); - initProgramState(_subCommand.opts()); - state.baseCommand = "add"; - }); - - addCommand.addCommand(makeAddAuthCommand()); - addCommand.addCommand(makeAddPageCommand()); - addCommand.addCommand(makeAddSchemaCommand()); - addCommand.addCommand(makeAddDataSourceCommand()); - return addCommand; -}; diff --git a/packages/cli-old/src/cli/add/page/index.ts b/packages/cli-old/src/cli/add/page/index.ts deleted file mode 100644 index 8ed95b1d..00000000 --- a/packages/cli-old/src/cli/add/page/index.ts +++ /dev/null @@ -1,230 +0,0 @@ -import path from "node:path"; -import chalk from "chalk"; -import { Command } from "commander"; -import { capitalize } from "es-toolkit"; -import fs from "fs-extra"; -import { nextjsTemplates, wvTemplates } from "~/cli/add/page/templates.js"; -import * as p from "~/cli/prompts.js"; -import { PKG_ROOT } from "~/consts.js"; -import { getExistingSchemas } from "~/generators/fmdapi.js"; -import { addRouteToNav } from "~/generators/route.js"; -import { ciOption, debugOption, nonInteractiveOption } from "~/globalOptions.js"; -import { initProgramState, isNonInteractiveMode, state } from "~/state.js"; -import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; -import { type DataSource, getSettings, mergeSettings } from "~/utils/parseSettings.js"; -import { abortIfCancel, ensureProofKitProject } from "../../utils.js"; - -export const runAddPageAction = async (opts?: { - routeName?: string; - pageName?: string; - dataSourceName?: string; - schemaName?: string; - template?: string; -}) => { - const projectDir = state.projectDir; - - const settings = getSettings(); - if (settings.ui === "shadcn") { - return p.cancel("Adding pages is not yet supported for shadcn-based projects."); - } - - const templates = state.appType === "browser" ? Object.entries(nextjsTemplates) : Object.entries(wvTemplates); - - if (templates.length === 0) { - return p.cancel("No templates found for your app type. Check back soon!"); - } - - let routeName = opts?.routeName; - let replacedMainPage = settings.replacedMainPage; - - if (state.appType === "webviewer" && !replacedMainPage && !isNonInteractiveMode() && !routeName) { - const replaceMainPage = abortIfCancel( - await p.select({ - message: "Do you want to replace the default page?", - options: [ - { label: "Yes", value: "yes" }, - { label: "No, maybe later", value: "no" }, - { label: "No, don't ask again", value: "never" }, - ], - }), - ); - if (replaceMainPage === "never" || replaceMainPage === "yes") { - replacedMainPage = true; - } - - if (replaceMainPage === "yes") { - routeName = "/"; - } - } - - if (!routeName) { - routeName = abortIfCancel( - await p.text({ - message: "Enter the URL PATH for your new page", - placeholder: "/my-page", - validate: (value) => { - if (value.length === 0) { - return "URL path is required"; - } - return; - }, - }), - ); - } - - if (!routeName.startsWith("/")) { - routeName = `/${routeName}`; - } - - const pageName = capitalize(routeName.replace("/", "").trim()); - - const template = - opts?.template ?? - abortIfCancel( - await p.select({ - message: "What template should be used for this page?", - options: templates.map(([key, value]) => ({ - value: key, - label: `${value.label}`, - hint: value.hint, - })), - }), - ); - - const pageTemplate = templates.find(([key]) => key === template)?.[1]; - if (!pageTemplate) { - return p.cancel(`Page template ${template} not found`); - } - - let dataSource: DataSource | undefined; - let schemaName: string | undefined; - if (pageTemplate.requireData) { - if (settings.dataSources.length === 0) { - return p.cancel( - "This template requires a data source, but you don't have any. Add a data source first, or choose another page template", - ); - } - - const dataSourceName = - opts?.dataSourceName ?? - (settings.dataSources.length > 1 - ? abortIfCancel( - await p.select({ - message: "Which data source should be used for this page?", - options: settings.dataSources.map((dataSource) => ({ - value: dataSource.name, - label: dataSource.name, - })), - }), - ) - : settings.dataSources[0]?.name); - - dataSource = settings.dataSources.find((dataSource) => dataSource.name === dataSourceName); - if (!dataSource) { - return p.cancel(`Data source ${dataSourceName} not found`); - } - - schemaName = await promptForSchemaFromDataSource({ - projectDir, - dataSource, - }); - } - - const spinner = p.spinner(); - spinner.start("Adding page from template"); - - // copy template files - const templatePath = path.join(PKG_ROOT, "template/pages", pageTemplate.templatePath); - - const destPath = - state.appType === "browser" - ? path.join(projectDir, "src/app/(main)", routeName) - : path.join(projectDir, "src/routes", routeName); - - await fs.copy(templatePath, destPath); - - if (state.appType === "browser") { - if (pageName && pageName !== "") { - await addRouteToNav({ - projectDir: process.cwd(), - navType: "primary", - label: pageName, - href: routeName, - }); - } - } else if (state.appType === "webviewer") { - // TODO: implement - } - // call post-install function - await pageTemplate.postIntallFn?.({ - projectDir, - pageDir: destPath, - dataSource, - schemaName, - }); - - if (replacedMainPage !== settings.replacedMainPage) { - // avoid changing this until the end since the user could cancel early - mergeSettings({ replacedMainPage }); - } - - spinner.stop("Added page!"); - const pkgManager = getUserPkgManager(); - - console.log( - `\n${chalk.green("Next steps:")}\nTo preview this page, restart your dev server using the ${chalk.cyan(`${pkgManager === "npm" ? "npm run" : pkgManager} dev`)} command\n`, - ); -}; - -export const makeAddPageCommand = () => { - const addPageCommand = new Command("page").description("Add a new page to your project").action(async () => { - await runAddPageAction(); - }); - - addPageCommand.addOption(ciOption); - addPageCommand.addOption(nonInteractiveOption); - addPageCommand.addOption(debugOption); - - addPageCommand.hook("preAction", () => { - initProgramState(addPageCommand.opts()); - state.baseCommand = "add"; - ensureProofKitProject({ commandName: "add" }); - }); - - return addPageCommand; -}; - -async function promptForSchemaFromDataSource({ - projectDir = process.cwd(), - dataSource, -}: { - projectDir?: string; - dataSource: DataSource; -}) { - if (dataSource.type === "supabase") { - throw new Error("Not implemented"); - } - const schemas = getExistingSchemas({ - projectDir, - dataSourceName: dataSource.name, - }) - .map((s) => s.schemaName) - .filter(Boolean); - - if (schemas.length === 0) { - p.cancel("This data source doesn't have any schemas to load data from"); - return undefined; - } - - if (schemas.length === 1) { - return schemas[0]; - } - - const schemaName = abortIfCancel( - await p.select({ - message: "Which schema should this page load data from?", - options: schemas.map((schema) => ({ label: schema ?? "", value: schema ?? "" })), - }), - ); - return schemaName; -} diff --git a/packages/cli-old/src/cli/add/page/post-install/table-infinite.ts b/packages/cli-old/src/cli/add/page/post-install/table-infinite.ts deleted file mode 100644 index fcccbb27..00000000 --- a/packages/cli-old/src/cli/add/page/post-install/table-infinite.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { injectTanstackQuery } from "~/generators/tanstack-query.js"; -import { installDependencies } from "~/helpers/installDependencies.js"; -import type { TPostInstallFn } from "../types.js"; -import { postInstallTable } from "./table.js"; - -export const postInstallTableInfinite: TPostInstallFn = async (args) => { - await postInstallTable(args); - const didInject = await injectTanstackQuery(); - if (didInject) { - await installDependencies(); - } -}; diff --git a/packages/cli-old/src/cli/add/page/post-install/table.ts b/packages/cli-old/src/cli/add/page/post-install/table.ts deleted file mode 100644 index a6e930ed..00000000 --- a/packages/cli-old/src/cli/add/page/post-install/table.ts +++ /dev/null @@ -1,123 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; -import { SyntaxKind } from "ts-morph"; - -import { getClientSuffix, getFieldNamesForSchema } from "~/generators/fmdapi.js"; -import { injectTanstackQuery } from "~/generators/tanstack-query.js"; -import { installDependencies } from "~/helpers/installDependencies.js"; -import { state } from "~/state.js"; -import { getSettings } from "~/utils/parseSettings.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; -import type { TPostInstallFn } from "../types.js"; - -// Regex to validate JavaScript identifiers -const VALID_JS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; - -export const postInstallTable: TPostInstallFn = async ({ projectDir, pageDir, dataSource, schemaName }) => { - if (!dataSource) { - throw new Error("DataSource is required for table page"); - } - if (!schemaName) { - throw new Error("SchemaName is required for table page"); - } - if (dataSource.type !== "fm") { - throw new Error("FileMaker DataSource is required for table page"); - } - - const clientSuffix = getClientSuffix({ - projectDir, - dataSourceName: dataSource.name, - }); - - const allFieldNames = getFieldNamesForSchema({ - schemaName, - dataSourceName: dataSource.name, - }); - - const settings = getSettings(); - if (settings.ui === "shadcn") { - return; - } - const auth = settings.auth; - - const substitutions = { - __SOURCE_NAME__: dataSource.name, - __TYPE_NAME__: `T${schemaName}`, - __ZOD_TYPE_NAME__: `Z${schemaName}`, - __CLIENT_NAME__: `${schemaName}${clientSuffix}`, - __SCHEMA_NAME__: schemaName, - __ACTION_CLIENT__: auth.type === "none" ? "actionClient" : "authedActionClient", - __FIRST_FIELD_NAME__: allFieldNames[0] ?? "NO_FIELDS_ON_YOUR_LAYOUT", - }; - - // read all files in pageDir and loop over them - const files = await fs.readdir(pageDir); - for await (const file of files) { - const filePath = path.join(pageDir, file); - let fileContent = await fs.readFile(filePath, "utf8"); - - for (const [key, value] of Object.entries(substitutions)) { - fileContent = fileContent.replace(new RegExp(key, "g"), value); - } - - await fs.writeFile(filePath, fileContent, "utf8"); - } - - // add the schemas to the columns array - const project = getNewProject(projectDir); - const sourceFile = project.addSourceFileAtPath( - path.join(pageDir, state.appType === "browser" ? "table.tsx" : "index.tsx"), - ); - const columns = sourceFile.getVariableDeclaration("columns")?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression); - - const fieldNames = filterOutCommonFieldNames(allFieldNames.filter(Boolean) as string[]); - - for await (const fieldName of fieldNames) { - columns?.addElement((writer) => - writer - .inlineBlock(() => { - if (needsBracketNotation(fieldName)) { - writer.write(`accessorFn: (row) => row["${fieldName}"],`); - } else { - writer.write(`accessorFn: (row) => row.${fieldName},`); - } - writer.write(`header: "${fieldName}",`); - }) - .write(",") - .newLine(), - ); - } - - if (state.appType === "webviewer") { - const didInject = await injectTanstackQuery({ project }); - if (didInject) { - await installDependencies(); - } - } - - await formatAndSaveSourceFiles(project); -}; - -// Function to check if a field name needs bracket notation -function needsBracketNotation(fieldName: string): boolean { - // Check if it's a valid JavaScript identifier - return !VALID_JS_IDENTIFIER.test(fieldName); -} - -const commonFieldNamesToExclude = [ - "id", - "pk", - "createdat", - "updatedat", - "primarykey", - "createdby", - "modifiedby", - "creationtimestamp", - "modificationtimestamp", -]; - -function filterOutCommonFieldNames(fieldNames: string[]): string[] { - return fieldNames.filter( - (fieldName) => !commonFieldNamesToExclude.includes(fieldName.toLowerCase()) || fieldName.startsWith("_"), - ); -} diff --git a/packages/cli-old/src/cli/add/page/templates.ts b/packages/cli-old/src/cli/add/page/templates.ts deleted file mode 100644 index a49d5740..00000000 --- a/packages/cli-old/src/cli/add/page/templates.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { postInstallTable } from "./post-install/table.js"; -import { postInstallTableInfinite } from "./post-install/table-infinite.js"; -import type { TPostInstallFn } from "./types.js"; - -export interface Template { - requireData: boolean; - label: string; - hint?: string; - templatePath: string; - screenshot?: string; - tags?: string[]; - postIntallFn?: TPostInstallFn; -} - -export const nextjsTemplates: Record = { - blank: { - requireData: false, - label: "Blank", - templatePath: "nextjs/blank", - }, - table: { - requireData: true, - label: "Basic Table", - hint: "Use to load and show multiple records", - templatePath: "nextjs/table", - postIntallFn: postInstallTable, - }, - tableEdit: { - requireData: true, - label: "Basic Table (editable)", - hint: "Use to load and show multiple records with inline edit functionality", - templatePath: "nextjs/table-edit", - postIntallFn: postInstallTable, - }, - tableInfinite: { - requireData: true, - label: "Infinite Table", - hint: "Automatically load more records when the user scrolls to the bottom", - templatePath: "nextjs/table-infinite", - postIntallFn: postInstallTableInfinite, - }, - tableInfiniteEdit: { - requireData: true, - label: "Infinite Table (editable)", - hint: "Automatically load more records when the user scrolls to the bottom with inline edit functionality", - templatePath: "nextjs/table-infinite-edit", - postIntallFn: postInstallTableInfinite, - }, -}; - -export const wvTemplates: Record = { - blank: { - requireData: false, - label: "Blank", - templatePath: "vite-wv/blank", - }, - table: { - requireData: true, - label: "Basic Table", - hint: "Use to load and show multiple records", - templatePath: "vite-wv/table", - postIntallFn: postInstallTable, - }, - tableEdit: { - requireData: true, - label: "Basic Table (editable)", - hint: "Use to load and show multiple records with inline edit functionality", - templatePath: "vite-wv/table-edit", - postIntallFn: postInstallTable, - }, - // tableInfinite: { - // requireData: true, - // label: "Infinite Table", - // hint: "Automatically load more records when the user scrolls to the bottom", - // templatePath: "vite-wv/table-infinite", - // postIntallFn: postInstallTableInfinite, - // }, - // tableInfiniteEdit: { - // requireData: true, - // label: "Infinite Table (editable)", - // hint: "Automatically load more records when the user scrolls to the bottom with inline edit functionality", - // templatePath: "vite-wv/table-infinite-edit", - // postIntallFn: postInstallTableInfinite, - // }, -}; diff --git a/packages/cli-old/src/cli/add/page/types.ts b/packages/cli-old/src/cli/add/page/types.ts deleted file mode 100644 index 7b7da162..00000000 --- a/packages/cli-old/src/cli/add/page/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { DataSource } from "~/utils/parseSettings.js"; - -export type TPostInstallFn = (args: { - projectDir: string; - /** Path in the project where the pages were copyied to. */ - pageDir: string; - dataSource?: DataSource; - schemaName?: string; -}) => void | Promise; - -export interface Template { - requireData: boolean; - label: string; - hint?: string; - /** Path from the template/pages directory to the template files to copy. */ - templatePath: string; - /** Will be run after the page contents is created and copied into the project. */ - postIntallFn?: TPostInstallFn; -} diff --git a/packages/cli-old/src/cli/add/registry/getOptions.ts b/packages/cli-old/src/cli/add/registry/getOptions.ts deleted file mode 100644 index 9e778b10..00000000 --- a/packages/cli-old/src/cli/add/registry/getOptions.ts +++ /dev/null @@ -1,44 +0,0 @@ -import path from "node:path"; -import fg from "fast-glob"; -import fs from "fs-extra"; - -import { state } from "~/state.js"; -import { registryFetch } from "./http.js"; - -export async function getMetaFromRegistry(name: string) { - const result = await registryFetch("@get/meta/:name", { - params: { name }, - }); - - if (result.error) { - if (result.error.status === 404) { - return null; - } - throw new Error(result.error.message); - } - - return result.data; -} - -const PROJECT_SHARED_IGNORE = ["**/node_modules/**", ".next", "public", "dist", "build"]; - -export async function getProjectInfo() { - const cwd = state.projectDir || process.cwd(); - const [configFiles, isSrcDir] = await Promise.all([ - fg.glob("**/{next,vite,astro,app}.config.*|gatsby-config.*|composer.json|react-router.config.*", { - cwd, - deep: 3, - ignore: PROJECT_SHARED_IGNORE, - }), - fs.pathExists(path.resolve(cwd, "src")), - ]); - - const isUsingAppDir = await fs.pathExists(path.resolve(cwd, `${isSrcDir ? "src/" : ""}app`)); - - // Next.js. - if (configFiles.find((file) => file.startsWith("next.config."))?.length) { - return isUsingAppDir ? "next-app" : "next-pages"; - } - - return "manual"; -} diff --git a/packages/cli-old/src/cli/add/registry/http.ts b/packages/cli-old/src/cli/add/registry/http.ts deleted file mode 100644 index 5625d73b..00000000 --- a/packages/cli-old/src/cli/add/registry/http.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createFetch, createSchema } from "@better-fetch/fetch"; -import { registryIndexSchema, templateMetadataSchema } from "@proofkit/registry"; - -import { getRegistryUrl } from "~/helpers/shadcn-cli.js"; - -const schema = createSchema({ - "@get/meta/:name": { - output: templateMetadataSchema, - }, - "@get/": { - output: registryIndexSchema, - }, -}); - -export const registryFetch = createFetch({ - baseURL: `${getRegistryUrl()}/r`, - schema, -}); diff --git a/packages/cli-old/src/cli/add/registry/install.ts b/packages/cli-old/src/cli/add/registry/install.ts deleted file mode 100644 index 368a84a0..00000000 --- a/packages/cli-old/src/cli/add/registry/install.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { getOtherProofKitDependencies } from "@proofkit/registry"; -import { capitalize, uniq } from "es-toolkit"; -import ora from "ora"; -import semver from "semver"; -import * as p from "~/cli/prompts.js"; - -import { abortIfCancel } from "~/cli/utils.js"; -import { getExistingSchemas } from "~/generators/fmdapi.js"; -import { addRouteToNav } from "~/generators/route.js"; -import { getRegistryUrl, shadcnInstall } from "~/helpers/shadcn-cli.js"; -import { state } from "~/state.js"; -import { getVersion } from "~/utils/getProofKitVersion.js"; -import { logger } from "~/utils/logger.js"; -import { type DataSource, getSettings, mergeSettings } from "~/utils/parseSettings.js"; -import { getMetaFromRegistry } from "./getOptions.js"; -import { buildHandlebarsData, randerHandlebarsToFile } from "./postInstall/handlebars.js"; -import { processPostInstallStep } from "./postInstall/index.js"; -import { preflightAddCommand } from "./preflight.js"; - -async function promptForSchemaFromDataSource({ - projectDir = process.cwd(), - dataSource, -}: { - projectDir?: string; - dataSource: DataSource; -}) { - if (dataSource.type === "supabase") { - throw new Error("Not implemented"); - } - const schemas = getExistingSchemas({ - projectDir, - dataSourceName: dataSource.name, - }) - .map((s) => s.schemaName) - .filter(Boolean); - - if (schemas.length === 0) { - p.cancel("This data source doesn't have any schemas to load data from"); - return undefined; - } - - if (schemas.length === 1) { - return schemas[0]; - } - - const schemaName = abortIfCancel( - await p.select({ - message: "Which schema should this template use?", - options: schemas.map((schema) => ({ label: schema ?? "", value: schema ?? "" })), - }), - ); - return schemaName; -} - -export async function installFromRegistry(name: string) { - const spinner = ora("Validating template").start(); - - try { - await preflightAddCommand(); - const meta = await getMetaFromRegistry(name); - if (!meta) { - spinner.fail(`Template ${name} not found in the ProofKit registry`); - return; - } - - if (meta.minimumProofKitVersion && semver.gt(meta.minimumProofKitVersion, getVersion())) { - logger.error( - `Template ${name} requires ProofKit version ${meta.minimumProofKitVersion}, but you are using version ${getVersion()}`, - ); - spinner.fail("Template is not compatible with your ProofKit version"); - return; - } - spinner.succeed(); - - const otherProofKitDependencies = getOtherProofKitDependencies(meta); - let previouslyInstalledTemplates = getSettings().registryTemplates; - - // Handle schema requirement if template needs it - let dataSource: DataSource | undefined; - let schemaName: string | undefined; - let routeName: string | undefined; - let pageName: string | undefined; - - if (meta.schemaRequired) { - const settings = getSettings(); - - if (settings.dataSources.length === 0) { - spinner.fail("This template requires a data source, but you don't have any. Add a data source first."); - return; - } - - const dataSourceName = - settings.dataSources.length > 1 - ? abortIfCancel( - await p.select({ - message: "Which data source should be used for this template?", - options: settings.dataSources.map((ds) => ({ - value: ds.name, - label: ds.name, - })), - }), - ) - : settings.dataSources[0]?.name; - - dataSource = settings.dataSources.find((ds) => ds.name === dataSourceName); - - if (!dataSource) { - spinner.fail(`Data source ${dataSourceName} not found`); - return; - } - - schemaName = await promptForSchemaFromDataSource({ - projectDir: state.projectDir, - dataSource, - }); - - if (!schemaName) { - spinner.fail("Schema selection was cancelled"); - return; - } - } - - if (meta.category === "page") { - // Prompt user for the URL path of the page - routeName = abortIfCancel( - await p.text({ - message: "Enter the URL PATH for your new page", - placeholder: "/my-page", - validate: (value) => { - if (value.length === 0) { - return "URL path is required"; - } - return; - }, - }), - ); - - if (routeName.startsWith("/")) { - routeName = routeName.slice(1); - } - - pageName = capitalize(routeName.replace("/", "").trim()); - } - - const url = new URL(`${getRegistryUrl()}/r/${name}`); - if (meta.category === "page") { - url.searchParams.set("routeName", `/(main)/${routeName ?? name}`); - } - - // a (hopefully) temporary workaround because the shadcn command installs the env file in the wrong place if it's a dependency - if ( - name === "fmdapi" && - !previouslyInstalledTemplates.includes("utils/t3-env") && - // this last guard will allow this workaroudn to be bypassed if the registry server updates to start serving the dependency again - meta.registryDependencies?.find((d) => d.includes("utils/t3-env")) === undefined - ) { - // install the t3-env template manually first - await installFromRegistry("utils/t3-env"); - previouslyInstalledTemplates = getSettings().registryTemplates; - } - - // now install the template using shadcn-install - await shadcnInstall([url.toString()], meta.title); - - const handlebarsFiles = meta.files.filter((file) => file.handlebars); - - if (handlebarsFiles.length > 0) { - // Build template data with schema information if available - const baseTemplateData = - dataSource && schemaName - ? buildHandlebarsData({ - dataSource, - schemaName, - }) - : buildHandlebarsData(); - - // Add page information to template data if available - const templateData = { - ...baseTemplateData, - ...(routeName && { routeName }), - ...(pageName && { pageName }), - }; - - // Resolve __PATH__ placeholders in file paths before handlebars processing - const resolvedFiles = handlebarsFiles.map((file) => ({ - ...file, - destinationPath: file.destinationPath?.replace("__PATH__", `/(main)/${routeName ?? name}`), - })); - - for (const file of resolvedFiles) { - await randerHandlebarsToFile(file, templateData); - } - } - - // Add route to navigation if this is a page template - if (meta.category === "page" && routeName && pageName) { - await addRouteToNav({ - projectDir: state.projectDir, - navType: "primary", - label: pageName, - href: `/${routeName}`, - }); - } - - // if post-install steps, process those - if (meta.postInstall) { - for (const step of meta.postInstall) { - if (step._from && previouslyInstalledTemplates.includes(step._from)) { - // don't re-run post-install steps for templates that have already been installed - continue; - } - await processPostInstallStep(step); - } - } - - // update the settings - mergeSettings({ - registryTemplates: uniq([...previouslyInstalledTemplates, name, ...otherProofKitDependencies]), - }); - } catch (error) { - spinner.fail("Failed to fetch template metadata."); - logger.error(error); - } -} diff --git a/packages/cli-old/src/cli/add/registry/listItems.ts b/packages/cli-old/src/cli/add/registry/listItems.ts deleted file mode 100644 index 046f5c73..00000000 --- a/packages/cli-old/src/cli/add/registry/listItems.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { registryFetch } from "./http.js"; - -export async function listItems() { - const { data: items, error } = await registryFetch("@get/"); - if (error) { - throw new Error(`Failed to fetch items from registry: ${error.message}`); - } - return items; -} diff --git a/packages/cli-old/src/cli/add/registry/postInstall/handlebars.ts b/packages/cli-old/src/cli/add/registry/postInstall/handlebars.ts deleted file mode 100644 index 2ef0b79c..00000000 --- a/packages/cli-old/src/cli/add/registry/postInstall/handlebars.ts +++ /dev/null @@ -1,189 +0,0 @@ -import path from "node:path"; -import { decodeHandlebarsFromShadcn, type TemplateFile } from "@proofkit/registry"; -import fs from "fs-extra"; -import handlebars from "handlebars"; -import { getClientSuffix, getFieldNamesForSchema } from "~/generators/fmdapi.js"; -import { getShadcnConfig } from "~/helpers/shadcn-cli.js"; -import { state } from "~/state.js"; -import { type DataSource, getSettings } from "~/utils/parseSettings.js"; - -// Register handlebars helpers -handlebars.registerHelper("eq", (a, b) => a === b); - -interface HandlebarsContext { - [key: string]: unknown; -} - -handlebars.registerHelper("findFirst", function (this: HandlebarsContext, array, predicate, options) { - if (!(array && Array.isArray(array))) { - return options.inverse(this); - } - - for (const item of array) { - if (predicate === "fm" && item.type === "fm") { - return options.fn(item); - } - } - return options.inverse(this); -}); - -interface DataSourceForTemplate { - dataSource: DataSource; - schemaName: string; -} - -const commonFieldNamesToExclude = [ - "id", - "pk", - "createdat", - "updatedat", - "primarykey", - "createdby", - "modifiedby", - "creationtimestamp", - "modificationtimestamp", -]; - -function filterOutCommonFieldNames(fieldNames: string[]): string[] { - return fieldNames.filter( - (fieldName) => !commonFieldNamesToExclude.includes(fieldName.toLowerCase()) || fieldName.startsWith("_"), - ); -} - -function buildDataSourceData(args: DataSourceForTemplate) { - const { dataSource, schemaName } = args; - - const clientSuffix = getClientSuffix({ - projectDir: state.projectDir ?? process.cwd(), - dataSourceName: dataSource.name, - }); - - const allFieldNames = getFieldNamesForSchema({ - schemaName, - dataSourceName: dataSource.name, - }).filter(Boolean) as string[]; - - return { - sourceName: dataSource.name, - schemaName, - clientSuffix, - allFieldNames, - fieldNames: filterOutCommonFieldNames(allFieldNames), - }; -} - -export function buildHandlebarsData(args?: DataSourceForTemplate) { - const proofkit = getSettings(); - const shadcn = getShadcnConfig(); - - return { - proofkit, - shadcn, - schema: args - ? buildDataSourceData(args) - : { - sourceName: "UnknownDataSource", - schemaName: "UnknownSchema", - clientSuffix: "UnknownClientSuffix", - allFieldNames: ["UnknownFieldName"], - fieldNames: ["UnknownFieldName"], - }, - }; -} - -export async function randerHandlebarsToFile(file: TemplateFile, data: ReturnType) { - const inputPath = getFilePath(file, data); - let rawTemplate = await fs.readFile(inputPath, "utf8"); - - // Decode placeholder tokens back to handlebars syntax - // This uses the centralized decoding function from the registry package - rawTemplate = decodeHandlebarsFromShadcn(rawTemplate); - - const template = handlebars.compile(rawTemplate); - const rendered = template(data); - await fs.writeFile(inputPath, rendered); -} - -export function getFilePath(file: TemplateFile, data: ReturnType): string { - const thePath = file.sourceFileName; - - if (file.destinationPath) { - return file.destinationPath; - } - - const cwd = state.projectDir ?? process.cwd(); - const { shadcn } = data; - - // Create a mapping between registry types and their corresponding shadcn config aliases - let blockAlias = "src/components/blocks"; - if (shadcn?.aliases?.components) { - if (shadcn.aliases.components.startsWith("@/")) { - blockAlias = `${shadcn.aliases.components.replace("@/", "src/")}/blocks`; - } else { - blockAlias = `src/${shadcn.aliases.components}/blocks`; - } - } - - const typeToAliasMap: Record = { - "registry:lib": shadcn?.aliases?.lib || shadcn?.aliases?.utils, - "registry:component": shadcn?.aliases?.components, - "registry:ui": shadcn?.aliases?.ui || shadcn?.aliases?.components, - "registry:hook": shadcn?.aliases?.hooks, - // These types don't have direct aliases, so we use fallback paths - "registry:file": "src", - "registry:page": "src/app", - "registry:block": blockAlias, - "registry:theme": "src/theme", - "registry:style": "src/styles", - }; - - const aliasPath = typeToAliasMap[file.type]; - - if (aliasPath) { - // Handle @/ prefix which represents the src directory - if (aliasPath.startsWith("@/")) { - const resolvedPath = aliasPath.replace("@/", "src/"); - return path.join(cwd, resolvedPath, thePath); - } - // If the alias starts with a path separator or contains src/, treat it as a relative path from cwd - if (aliasPath.startsWith("/") || aliasPath.includes("src/")) { - return path.join(cwd, aliasPath, thePath); - } - // Otherwise, treat it as an alias that should be resolved relative to src/ - - return path.join(cwd, "src", aliasPath, thePath); - } - - // Fallback to hardcoded paths for unsupported types - switch (file.type) { - case "registry:lib": - return path.join(cwd, "src", "lib", thePath); - case "registry:file": - return path.join(cwd, "src", thePath); - case "registry:page": { - // For page templates, use the route name if available in template data - const routeName = "routeName" in data ? (data.routeName as string) : undefined; - if (routeName) { - // Add /(main) prefix for Next.js app router structure - const pageRoute = routeName === "/" ? "" : routeName; - return path.join(cwd, "src", "app", "(main)", pageRoute, thePath); - } - return path.join(cwd, "src", "app", thePath); - } - case "registry:block": - return path.join(cwd, "src", "components", "blocks", thePath); - case "registry:component": - return path.join(cwd, "src", "components", thePath); - case "registry:ui": - return path.join(cwd, "src", "components", thePath); - case "registry:hook": - return path.join(cwd, "src", "hooks", thePath); - case "registry:theme": - return path.join(cwd, "src", "theme", thePath); - case "registry:style": - return path.join(cwd, "src", "styles", thePath); - default: - // default to source file name - return thePath; - } -} diff --git a/packages/cli-old/src/cli/add/registry/postInstall/index.ts b/packages/cli-old/src/cli/add/registry/postInstall/index.ts deleted file mode 100644 index 6afb4233..00000000 --- a/packages/cli-old/src/cli/add/registry/postInstall/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { PostInstallStep } from "@proofkit/registry"; - -import { addToEnv } from "~/utils/addToEnvs.js"; -import { logger } from "~/utils/logger.js"; -import { addScriptToPackageJson } from "./package-script.js"; -import { wrapProvider } from "./wrap-provider.js"; - -export async function processPostInstallStep(step: PostInstallStep) { - if (step.action === "package.json script") { - addScriptToPackageJson(step); - } else if (step.action === "wrap provider") { - await wrapProvider(step); - } else if (step.action === "next-steps") { - logger.info(step.data.message); - } else if (step.action === "env") { - await addToEnv({ - envs: step.data.envs, - }); - } else { - logger.error(`Unknown post-install step: ${step}`); - } -} diff --git a/packages/cli-old/src/cli/add/registry/postInstall/package-script.ts b/packages/cli-old/src/cli/add/registry/postInstall/package-script.ts deleted file mode 100644 index 50df220c..00000000 --- a/packages/cli-old/src/cli/add/registry/postInstall/package-script.ts +++ /dev/null @@ -1,12 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import type { PostInstallStep } from "@proofkit/registry"; - -import { state } from "~/state.js"; - -export function addScriptToPackageJson(step: Extract) { - const packageJsonPath = path.join(state.projectDir, "package.json"); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); - packageJson.scripts[step.data.scriptName] = step.data.scriptCommand; - fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); -} diff --git a/packages/cli-old/src/cli/add/registry/postInstall/wrap-provider.ts b/packages/cli-old/src/cli/add/registry/postInstall/wrap-provider.ts deleted file mode 100644 index dd1ec34e..00000000 --- a/packages/cli-old/src/cli/add/registry/postInstall/wrap-provider.ts +++ /dev/null @@ -1,132 +0,0 @@ -import path from "node:path"; -import type { PostInstallStep } from "@proofkit/registry"; -import { type ImportDeclarationStructure, type JsxChild, type JsxElement, StructureKind, SyntaxKind } from "ts-morph"; - -import { getShadcnConfig } from "~/helpers/shadcn-cli.js"; -import { state } from "~/state.js"; -import { logger } from "~/utils/logger.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; - -export async function wrapProvider(step: Extract) { - const { parentTag, imports: importConfigs, providerCloseTag, providerOpenTag } = step.data; - - try { - const projectDir = state.projectDir; - const project = getNewProject(projectDir); - const shadcnConfig = getShadcnConfig(); - - // Resolve the components alias to a filesystem path - // @/components -> src/components, ./components -> components, etc. - const resolveAlias = (alias: string): string => { - if (alias.startsWith("@/")) { - return alias.replace("@/", "src/"); - } - if (alias.startsWith("./")) { - return alias.substring(2); - } - return alias; - }; - - // Look for providers.tsx in the components directory - const componentsDir = resolveAlias(shadcnConfig.aliases.components); - const providersPath = path.join(projectDir, componentsDir, "providers.tsx"); - - const providersFile = project.addSourceFileAtPath(providersPath); - - // Add all import statements - for (const importConfig of importConfigs) { - const importDeclaration: ImportDeclarationStructure = { - moduleSpecifier: importConfig.moduleSpecifier, - kind: StructureKind.ImportDeclaration, - }; - - if (importConfig.defaultImport) { - importDeclaration.defaultImport = importConfig.defaultImport; - } - - if (importConfig.namedImports && importConfig.namedImports.length > 0) { - importDeclaration.namedImports = importConfig.namedImports; - } - - providersFile.addImportDeclaration(importDeclaration); - } - - // Handle providers.tsx file - look for the default export function - const exportDefault = providersFile.getFunction((dec) => dec.isDefaultExport()); - - if (!exportDefault) { - logger.warn(`No default export function found in ${providersPath}`); - return; - } - - const returnStatement = exportDefault?.getBody()?.getFirstDescendantByKind(SyntaxKind.ReturnStatement); - - if (!returnStatement) { - logger.warn("No return statement found in default export function"); - return; - } - - let targetElement: JsxElement | undefined; - - // Try to find the parent tag if specified - if (parentTag && parentTag.length > 0) { - for (const tag of parentTag) { - targetElement = returnStatement - ?.getDescendantsOfKind(SyntaxKind.JsxOpeningElement) - .find((openingElement) => openingElement.getTagNameNode().getText() === tag) - ?.getParentIfKind(SyntaxKind.JsxElement); - - if (targetElement) { - break; - } - } - } - - if (targetElement) { - // If we found a parent tag, wrap its children - const childrenText = targetElement - ?.getJsxChildren() - .map((child: JsxChild) => child.getText()) - .filter(Boolean) - .join("\n"); - - const newContent = `${providerOpenTag} - ${childrenText} - ${providerCloseTag}`; - - targetElement.getChildSyntaxList()?.replaceWithText(newContent); - } else { - // If no parent tag found or specified, wrap the entire return statement - const returnExpression = returnStatement?.getExpression(); - if (returnExpression) { - // Check if the expression is a ParenthesizedExpression - const isParenthesized = returnExpression.getKind() === SyntaxKind.ParenthesizedExpression; - - let innerExpressionText: string; - if (isParenthesized) { - // Get the inner expression from the parenthesized expression - const parenthesizedExpr = returnExpression.asKindOrThrow(SyntaxKind.ParenthesizedExpression); - innerExpressionText = parenthesizedExpr.getExpression().getText(); - } else { - innerExpressionText = returnExpression.getText(); - } - - const newReturnContent = `return ( - ${providerOpenTag} - ${innerExpressionText} - ${providerCloseTag} - );`; - - returnStatement?.replaceWithText(newReturnContent); - } else { - logger.warn("No return expression found to wrap"); - } - } - - await formatAndSaveSourceFiles(project); - logger.success(`Successfully wrapped provider in ${providersPath}`); - } catch (error) { - logger.error(`Failed to wrap provider: ${error}`); - throw error; - } -} diff --git a/packages/cli-old/src/cli/add/registry/preflight.ts b/packages/cli-old/src/cli/add/registry/preflight.ts deleted file mode 100644 index a7e973f0..00000000 --- a/packages/cli-old/src/cli/add/registry/preflight.ts +++ /dev/null @@ -1,17 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; - -import { stealthInit } from "~/helpers/stealth-init.js"; -import { state } from "~/state.js"; - -export async function preflightAddCommand() { - const cwd = state.projectDir ?? process.cwd(); - // make sure shadcn is installed, throw if not - const shadcnInstalled = await fs.pathExists(path.join(cwd, "components.json")); - if (!shadcnInstalled) { - throw new Error("Shadcn is not installed. Please run `pnpm dlx shadcn@latest init` to install it."); - } - - // if proofkit is not inited, try to stealth init - await stealthInit(); -} diff --git a/packages/cli-old/src/cli/deploy/index.ts b/packages/cli-old/src/cli/deploy/index.ts deleted file mode 100644 index ecc1deac..00000000 --- a/packages/cli-old/src/cli/deploy/index.ts +++ /dev/null @@ -1,489 +0,0 @@ -import path from "node:path"; -import chalk from "chalk"; -import { Command, Option } from "commander"; -import { execa } from "execa"; -import fs from "fs-extra"; -import type { PackageJson } from "type-fest"; -import * as p from "~/cli/prompts.js"; - -import { ciOption, debugOption } from "~/globalOptions.js"; - -// Regex patterns defined at top level for performance -const LEADING_SYMBOLS_REGEX = /^[โœ”\s]+/; -const MULTI_SPACE_REGEX = /\s{2,}/; -const VERSION_PREFIX_REGEX = /^v/; - -import { initProgramState, state } from "~/state.js"; -import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; -import { getSettings } from "~/utils/parseSettings.js"; -import { ensureProofKitProject } from "../utils.js"; - -async function checkVercelCLI(): Promise { - try { - await execa("vercel", ["--version"]); - return true; - } catch (_error) { - return false; - } -} - -async function installVercelCLI() { - const pkgManager = getUserPkgManager(); - const spinner = p.spinner(); - spinner.start("Installing Vercel CLI..."); - - try { - const installCmd = pkgManager === "npm" ? "install" : "add"; - await execa(pkgManager, [installCmd, "-g", "vercel"]); - spinner.stop("Vercel CLI installed successfully"); - return true; - } catch (error) { - spinner.stop("Failed to install Vercel CLI"); - console.error(chalk.red("Error installing Vercel CLI:"), error); - return false; - } -} - -async function checkVercelProject(): Promise { - try { - // Try to read the .vercel/project.json file which exists when a project is linked - const projectConfig = (await fs.readJSON(".vercel/project.json")) as VercelProjectConfig; - return Boolean(projectConfig.projectId); - } catch (_error) { - if (state.debug) { - console.log("\nDebug: No Vercel project configuration found"); - } - return false; - } -} - -async function getVercelTeams(): Promise<{ slug: string; name: string }[]> { - try { - if (state.debug) { - console.log("\nDebug: Running vercel teams list command..."); - } - - const result = await execa("vercel", ["teams", "list"], { - all: true, - }); - - if (state.debug) { - console.log("\nDebug: Command output:", result.all); - } - - const lines = (result.all ?? "").split("\n").filter(Boolean); - - // Find the index of the header line - const headerIndex = lines.findIndex((line) => line.includes("id")); - if (headerIndex === -1) { - return []; - } - - // Get only the lines after the header - const teamLines = lines.slice(headerIndex + 1); - - if (state.debug) { - console.log("\nDebug: Team lines:"); - for (const line of teamLines) { - console.log(`"${line}"`); - } - } - - const teams = teamLines - .map((line) => { - // Remove any leading symbols (โœ” or spaces) and trim - const cleanLine = line.replace(LEADING_SYMBOLS_REGEX, "").trim(); - // Split on multiple spaces and take the first part as slug, rest as name - const [slug, ...nameParts] = cleanLine.split(MULTI_SPACE_REGEX); - if (!slug || nameParts.length === 0) { - return null; - } - - return { - slug, - name: nameParts.join(" ").trim(), - }; - }) - .filter((team): team is { slug: string; name: string } => team !== null); - - if (state.debug) { - console.log("\nDebug: Parsed teams:", teams); - } - - return teams; - } catch (error) { - if (state.debug) { - console.error("Error getting Vercel teams:", error); - } - return []; - } -} - -async function setupVercelProject() { - const spinner = p.spinner(); - - try { - // Get project name from package.json - const pkgJson = (await fs.readJSON("package.json")) as PackageJson; - const projectName = pkgJson.name; - - // Get available teams - const teams = await getVercelTeams(); - - let teamFlag = ""; - if (teams.length > 1) { - const teamChoice = await p.select({ - message: "Select a team to deploy under:", - options: [ - ...teams.map((team) => ({ - value: team.slug, - label: team.name, - })), - ], - }); - - if (p.isCancel(teamChoice)) { - console.log(chalk.yellow("\nOperation cancelled")); - return false; - } - - if (teamChoice && typeof teamChoice === "string") { - teamFlag = `--scope=${teamChoice}`; - } - } - - spinner.start("Creating Vercel project..."); - - // Create project with default settings - await execa("vercel", ["link", "--yes", ...(teamFlag ? [teamFlag] : [])], { - env: { - VERCEL_PROJECT_NAME: projectName, - }, - }); - - // Pull project settings - spinner.message("Pulling project settings..."); - await execa("vercel", ["pull", "--yes"], { - stdio: "inherit", - }); - - spinner.stop("Vercel project created successfully"); - return true; - } catch (error) { - spinner.stop("Failed to set up Vercel project"); - console.error(chalk.red("Error setting up Vercel project:"), error); - return false; - } -} - -async function pushEnvironmentVariables() { - const spinner = p.spinner(); - spinner.start("Pushing environment variables to Vercel..."); - - try { - const settings = getSettings(); - const envFile = path.join(process.cwd(), settings.envFile ?? ".env"); - - if (!fs.existsSync(envFile)) { - spinner.stop("No environment file found"); - return true; - } - - const envContent = await fs.readFile(envFile, "utf-8"); - const envVars = envContent - .split("\n") - .filter((line) => line.trim() && !line.startsWith("#")) - .map((line) => { - const [key, ...valueParts] = line.split("="); - if (!key) { - return null; - } - const value = valueParts.join("="); // Rejoin in case value contains = - return { key: key.trim(), value: value.trim() }; - }) - .filter((item): item is { key: string; value: string } => item !== null); - - if (state.debug) { - spinner.stop(); - console.log("\nDebug: Parsed environment variables:"); - for (const { key, value } of envVars) { - console.log(` ${key}=${value.substring(0, 3)}...`); - } - spinner.start("Pushing environment variables to Vercel..."); - } - - let failed = 0; - const total = envVars.length; - - for (let i = 0; i < total; i++) { - const envVar = envVars[i]; - if (!envVar) { - continue; - } - const { key, value } = envVar; - spinner.message(`Pushing environment variables to Vercel... (${i + 1}/${total})`); - - try { - if (state.debug) { - console.log(`\nDebug: Attempting to add ${key} to Vercel...`); - } - - const result = await execa("vercel", ["env", "add", key, "production"], { - input: value, - stdio: "pipe", - reject: false, - }); - - if (state.debug) { - console.log(`Debug: Command exit code: ${result.exitCode}`); - if (result.stdout) { - console.log("Debug: stdout:", result.stdout); - } - if (result.stderr) { - console.log("Debug: stderr:", result.stderr); - } - } - - if (result.exitCode !== 0) { - throw new Error(`Command failed with exit code ${result.exitCode}`); - } - } catch (error) { - failed++; - if (state.debug) { - console.error(chalk.yellow(`\nDebug: Failed to add ${key}`)); - console.error("Debug: Full error:", error); - } - } - } - - if (failed > 0) { - spinner.stop(chalk.yellow(`Environment variables pushed with ${failed} failures`)); - } else { - spinner.stop("Environment variables pushed successfully"); - } - return failed < total; - } catch (error) { - spinner.stop("Failed to push environment variables"); - if (state.debug) { - console.error("\nDebug: Top-level error in pushEnvironmentVariables:"); - console.error(error); - } - return false; - } -} - -interface VercelProjectConfig { - projectId: string; - settings?: { - nodeVersion?: string; - }; - [key: string]: unknown; -} - -async function ensureCorrectNodeVersion() { - const nodeVersion = process.version.replace(VERSION_PREFIX_REGEX, ""); - const majorVersion = nodeVersion.split(".")[0]; - - try { - const projectJsonPath = ".vercel/project.json"; - if (!fs.existsSync(projectJsonPath)) { - if (state.debug) { - console.log("Debug: No project.json found"); - } - return false; - } - - const projectConfig = (await fs.readJSON(projectJsonPath)) as VercelProjectConfig; - if (state.debug) { - console.log("Debug: Current project config:", projectConfig); - } - - // Update the Node.js version - projectConfig.settings = { - ...projectConfig.settings, - nodeVersion: `${majorVersion}.x`, - }; - - await fs.writeJSON(projectJsonPath, projectConfig, { spaces: 2 }); - if (state.debug) { - console.log(`Debug: Updated Node.js version to ${majorVersion}.x`); - } - return true; - } catch (error) { - if (state.debug) { - console.error("Debug: Failed to update Node.js version:", error); - } - return false; - } -} - -async function checkVercelLogin(): Promise { - try { - const result = await execa("vercel", ["whoami"], { - stdio: "pipe", - reject: false, - }); - - if (state.debug) { - console.log("\nDebug: Vercel whoami result:", result); - } - - return result.exitCode === 0; - } catch (error) { - if (state.debug) { - console.error("Debug: Error checking Vercel login status:", error); - } - return false; - } -} - -async function loginToVercel(): Promise { - console.log(chalk.blue("\nYou need to log in to Vercel first.")); - - try { - await execa("vercel", ["login"], { - stdio: "inherit", - }); - return true; - } catch (error) { - console.error(chalk.red("\nFailed to log in to Vercel:"), error); - return false; - } -} - -export async function runDeploy() { - if (state.debug) { - console.log("Running deploy..."); - } - - // Check if Vercel CLI is installed - const hasVercelCLI = await checkVercelCLI(); - - if (!hasVercelCLI) { - const installed = await installVercelCLI(); - if (!installed) { - console.log(chalk.red("\nFailed to install Vercel CLI. Please install it manually using:")); - console.log(chalk.blue("\n npm install -g vercel")); - return; - } - } - - // Check if user is logged in - const isLoggedIn = await checkVercelLogin(); - if (!isLoggedIn) { - const loginSuccessful = await loginToVercel(); - if (!loginSuccessful) { - console.log(chalk.red("\nFailed to log in to Vercel. Please try again.")); - return; - } - } - - // Check if project is set up with Vercel - const hasVercelProject = await checkVercelProject(); - - if (!hasVercelProject) { - console.log(chalk.blue("\nSetting up new Vercel project...")); - const setup = await setupVercelProject(); - if (!setup) { - console.log(chalk.red("\nFailed to set up Vercel project automatically.")); - return; - } - - const envPushed = await pushEnvironmentVariables(); - if (!envPushed) { - console.log(chalk.red("\nFailed to push environment variables. Aborting deployment.")); - return; - } - } - - // Pull latest project settings - console.log(chalk.blue("\nPulling latest project settings...")); - try { - await execa("vercel", ["pull", "--yes"], { - stdio: "inherit", - }); - } catch (error) { - console.error(chalk.red("\nFailed to pull project settings:"), error); - return; - } - - // Ensure correct Node.js version is set - if (!(await ensureCorrectNodeVersion())) { - console.error(chalk.red("\nFailed to set Node.js version. Continuing anyway...")); - } - - if (state.localBuild) { - // Build locally for Vercel - console.log(chalk.blue("\nPreparing local build for Vercel...")); - try { - const result = await execa("vercel", ["build"], { - stdio: "inherit", - reject: false, - }); - if (result.exitCode === 0) { - console.log(chalk.green("\nโœ“ Local build successful!")); - } else { - console.error(chalk.red("\nโœ– Local build failed")); - console.log(chalk.yellow("Fix the errors above and then try again.")); - return; - } - } catch (error) { - console.error(chalk.red("\nVercel build failed:"), error); - return; - } - - // Deploy the pre-built project - console.log(chalk.blue("\nDeploying to Vercel...")); - - const result = await execa("vercel", ["deploy", "--prebuilt", "--yes"], { - stdio: "inherit", - reject: false, - }); - if (result.exitCode === 0) { - console.log(chalk.green("\nโœ“ Deployment successful!")); - } - } else { - // Deploy and build on Vercel - console.log(chalk.blue("\nDeploying to Vercel...")); - try { - const result = await execa("vercel", ["deploy", "--yes"], { - stdio: "inherit", - reject: false, - }); - if (result.exitCode === 0) { - console.log(chalk.green("\nโœ“ Deployment successful!")); - } else { - const pkgManager = getUserPkgManager(); - const runCmd = pkgManager === "npm" ? "npm run" : pkgManager; - console.error(chalk.red("\nโœ– Deployment failed")); - - console.log(chalk.yellow("\nTroubleshooting Tips:")); - console.log(chalk.dim("You can check for most errors before deploying for a faster iteration cycle")); - console.log( - `${chalk.dim("Run")} ${runCmd} tsc ${chalk.dim("to check for TypeScript errors (most common build errors)")}`, - ); - console.log(`${chalk.dim("Run")} ${runCmd} build ${chalk.dim("to run the full production build locally")}`); - } - } catch { - // This catch block should rarely be hit since we're using reject: false - return; - } - } -} - -export const makeDeployCommand = () => { - const deployCommand = new Command("deploy") - .description("Deploy your ProofKit application to Vercel") - .addOption(ciOption) - .addOption(debugOption) - .addOption(new Option("--local-build", "Build locally before deploying")) - .action(runDeploy); - - deployCommand.hook("preAction", (thisCommand) => { - initProgramState(thisCommand.opts()); - state.baseCommand = "deploy"; - ensureProofKitProject({ commandName: "deploy" }); - }); - - return deployCommand; -}; diff --git a/packages/cli-old/src/cli/fmdapi.ts b/packages/cli-old/src/cli/fmdapi.ts deleted file mode 100644 index cb252b8a..00000000 --- a/packages/cli-old/src/cli/fmdapi.ts +++ /dev/null @@ -1,57 +0,0 @@ -import DataApi, { type clientTypes, OttoAdapter, type OttoAPIKey } from "@proofkit/fmdapi"; - -export async function getLayouts({ - dataApiKey, - fmFile, - server, -}: { - dataApiKey: OttoAPIKey; - fmFile: string; - server: string; -}) { - const DapiClient = DataApi({ - adapter: new OttoAdapter({ - auth: { apiKey: dataApiKey }, - db: fmFile, - server, - }), - layout: "", - }); - - const layoutsResp = await DapiClient.layouts(); - - const layouts = transformLayoutList(layoutsResp.layouts); - - return layouts; -} - -function getAllLayoutNames(layout: clientTypes.LayoutOrFolder): string[] { - if ("isFolder" in layout) { - return (layout.folderLayoutNames ?? []).flatMap(getAllLayoutNames); - } - return [layout.name]; -} - -export const commonFileMakerLayoutPrefixes = ["API_", "API ", "dapi_", "dapi"]; - -export function transformLayoutList(layouts: clientTypes.LayoutOrFolder[]): string[] { - const flatList = layouts.flatMap(getAllLayoutNames); - - // sort the list so that any values that begin with one of the prefixes are at the top - - const sortedList = flatList.sort((a, b) => { - const aPrefix = commonFileMakerLayoutPrefixes.find((prefix) => a.startsWith(prefix)); - const bPrefix = commonFileMakerLayoutPrefixes.find((prefix) => b.startsWith(prefix)); - if (aPrefix && bPrefix) { - return a.localeCompare(b); - } - if (aPrefix) { - return -1; - } - if (bPrefix) { - return 1; - } - return a.localeCompare(b); - }); - return sortedList; -} diff --git a/packages/cli-old/src/cli/init.ts b/packages/cli-old/src/cli/init.ts deleted file mode 100644 index 4b8cc21c..00000000 --- a/packages/cli-old/src/cli/init.ts +++ /dev/null @@ -1,395 +0,0 @@ -import path from "node:path"; -import { Command } from "commander"; -import { execa } from "execa"; -import fs from "fs-extra"; -import type { PackageJson } from "type-fest"; - -import { DEFAULT_APP_NAME } from "~/consts.js"; -import { addAuth } from "~/generators/auth.js"; -import { runCodegenCommand } from "~/generators/fmdapi.js"; -import { ciOption, debugOption, nonInteractiveOption } from "~/globalOptions.js"; -import { createBareProject } from "~/helpers/createProject.js"; -import { initializeGit } from "~/helpers/git.js"; -import { installDependencies } from "~/helpers/installDependencies.js"; -import { logNextSteps } from "~/helpers/logNextSteps.js"; -import { setImportAlias } from "~/helpers/setImportAlias.js"; -import { buildPkgInstallerMap } from "~/installers/index.js"; -import { initProgramState, isNonInteractiveMode, state } from "~/state.js"; -import { getVersion } from "~/utils/getProofKitVersion.js"; -import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; -import { parseNameAndPath } from "~/utils/parseNameAndPath.js"; -import { type Settings, setSettings } from "~/utils/parseSettings.js"; -import { validateAppName } from "~/utils/validateAppName.js"; -import { promptForFileMakerDataSource } from "./add/data-source/filemaker.js"; -import { select, text } from "./prompts.js"; -import { abortIfCancel } from "./utils.js"; - -interface CliFlags { - noGit: boolean; - noInstall: boolean; - force: boolean; - default: boolean; - importAlias: string; - server?: string; - adminApiKey?: string; - fileName: string; - layoutName: string; - schemaName: string; - dataApiKey: string; - fmServerURL: string; - auth: "none" | "next-auth" | "clerk"; - dataSource?: "filemaker" | "none" | "supabase"; - /** @internal UI library selection; hidden flag */ - ui?: "shadcn" | "mantine"; - /** @internal Used in CI. */ - CI: boolean; - /** @internal Used in non-interactive mode. */ - nonInteractive?: boolean; - /** @internal Used in CI. */ - tailwind: boolean; - /** @internal Used in CI. */ - trpc: boolean; - /** @internal Used in CI. */ - prisma: boolean; - /** @internal Used in CI. */ - drizzle: boolean; - /** @internal Used in CI. */ - appRouter: boolean; -} - -const defaultOptions: CliFlags = { - noGit: false, - noInstall: false, - force: false, - default: false, - CI: false, - tailwind: false, - trpc: false, - prisma: false, - drizzle: false, - importAlias: "~/", - appRouter: false, - auth: "none", - server: undefined, - fileName: "", - layoutName: "", - schemaName: "", - dataApiKey: "", - fmServerURL: "", - dataSource: undefined, - ui: "shadcn", -}; - -export const makeInitCommand = () => { - const initCommand = new Command("init") - .description("Create a new project with ProofKit") - .argument("[dir]", "The name of the application, as well as the name of the directory to create") - .option("--appType [type]", "The type of app to create", undefined) - // hidden UI selector; default is shadcn; pass --ui mantine to opt-in legacy Mantine templates - .option("--ui [ui]", undefined, undefined) - .option("--server [url]", "The URL of your FileMaker Server", undefined) - .option("--adminApiKey [key]", "Admin API key for OttoFMS. If provided, will skip login prompt", undefined) - .option("--fileName [name]", "The name of the FileMaker file to use for the web app", undefined) - .option("--layoutName [name]", "The name of the FileMaker layout to use for the web app", undefined) - .option("--schemaName [name]", "The name for the generated layout client in your schemas", undefined) - .option("--dataApiKey [key]", "The API key to use for the FileMaker Data API", undefined) - .option("--auth [type]", "The authentication provider to use for the web app", undefined) - .option("--dataSource [type]", "The data source to use for the web app (filemaker or none)", undefined) - .option("--noGit", "Explicitly tell the CLI to not initialize a new git repo in the project", false) - .option("--noInstall", "Explicitly tell the CLI to not run the package manager's install command", false) - .option("-f, --force", "Force overwrite target directory when it already contains files", false) - .addOption(ciOption) - .addOption(nonInteractiveOption) - .addOption(debugOption) - .action(runInit); - - initCommand.hook("preAction", (cmd) => { - initProgramState(cmd.opts()); - state.baseCommand = "init"; - }); - - return initCommand; -}; - -async function askForAuth({ projectDir }: { projectDir: string }) { - const authType = "none" as "none" | "clerk" | "fmaddon"; - if (authType === "clerk") { - await addAuth({ - options: { type: "clerk" }, - projectDir, - noInstall: true, - }); - } else if (authType === "fmaddon") { - await addAuth({ - options: { type: "fmaddon" }, - projectDir, - noInstall: true, - }); - } -} - -type ProofKitPackageJSON = PackageJson & { - proofkitMetadata?: { - initVersion: string; - }; -}; - -const missingTypegenCommandPatterns = [ - /ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL[\s\S]*Command\s+["'`]typegen["'`]\s+not found/i, - /Command\s+["'`]typegen["'`]\s+not found/i, - /Missing script:\s*["'`]typegen["'`]/i, - /Script not found\s*["'`]typegen["'`]/i, -]; - -function getErrorMessage(error: unknown): string { - if (error instanceof Error) { - return error.message; - } - return String(error); -} - -export function isMissingTypegenCommandError(error: unknown): boolean { - const message = getErrorMessage(error); - return missingTypegenCommandPatterns.some((pattern) => pattern.test(message)); -} - -export function createPostInitGenerationError({ - error, - appType, - projectDir, -}: { - error: unknown; - appType: "browser" | "webviewer"; - projectDir: string; -}) { - const rootError = error instanceof Error ? error : new Error(getErrorMessage(error)); - - if (appType === "browser" && isMissingTypegenCommandError(error)) { - return new Error( - [ - "Post-init generation failed after scaffolding.", - `Project created at: ${projectDir}`, - "Root cause: a `typegen` package command was invoked, but browser scaffolds do not define that script.", - "Continue using the generated project, then run `proofkit typegen` later after FileMaker setup is complete.", - ].join("\n"), - { cause: rootError }, - ); - } - - return new Error( - [ - "Post-init generation failed after scaffolding.", - `Project created at: ${projectDir}`, - "Retry `proofkit typegen` from inside the project once FileMaker settings and connectivity are valid.", - `Underlying error: ${getErrorMessage(error)}`, - ].join("\n"), - { cause: rootError }, - ); -} - -export const runInit = async (name?: string, opts?: CliFlags) => { - const pkgManager = getUserPkgManager(); - const cliOptions = opts ?? defaultOptions; - const nonInteractive = isNonInteractiveMode(); - const noInstall = cliOptions.noInstall ?? (opts as { install?: boolean } | undefined)?.install === false; - const noGit = cliOptions.noGit ?? (opts as { git?: boolean } | undefined)?.git === false; - // capture ui choice early into state - state.ui = (cliOptions.ui ?? "shadcn") as "shadcn" | "mantine"; - - let projectName = name; - if (!projectName) { - if (nonInteractive) { - throw new Error("Project name is required in non-interactive mode."); - } - projectName = abortIfCancel( - await text({ - message: "What will your project be called?", - defaultValue: DEFAULT_APP_NAME, - validate: validateAppName, - }), - ).toString(); - } - - const appNameValidation = validateAppName(projectName); - if (appNameValidation) { - throw new Error(appNameValidation); - } - - const hasExplicitFileMakerInputs = Boolean( - cliOptions.server || - cliOptions.adminApiKey || - cliOptions.dataApiKey || - cliOptions.fileName || - cliOptions.layoutName || - cliOptions.schemaName, - ); - const hasPartialFileMakerSchemaInputs = Boolean(cliOptions.layoutName) !== Boolean(cliOptions.schemaName); - - if (!state.appType) { - state.appType = nonInteractive - ? "browser" - : (abortIfCancel( - await select({ - message: "What kind of app do you want to build?", - options: [ - { - value: "browser", - label: "Web App for Browsers", - hint: "Uses Next.js, will require hosting", - }, - { - value: "webviewer", - label: "FileMaker Web Viewer (beta)", - hint: "Uses Vite, can be embedded in FileMaker or hosted", - }, - ], - }), - ) as "browser" | "webviewer"); - } - - if (nonInteractive && hasPartialFileMakerSchemaInputs) { - throw new Error("Both --layoutName and --schemaName must be provided together."); - } - - if (nonInteractive && hasExplicitFileMakerInputs) { - const resolvedDataSourceForValidation = - state.appType === "webviewer" - ? (cliOptions.dataSource ?? (cliOptions.server ? "filemaker" : "none")) - : (cliOptions.dataSource ?? "none"); - - if (resolvedDataSourceForValidation !== "filemaker") { - throw new Error("FileMaker flags require --dataSource filemaker in non-interactive mode."); - } - } - - const usePackages = buildPkgInstallerMap(); - - // e.g. dir/@mono/app returns ["@mono/app", "dir/app"] - const [scopedAppName, appDir] = parseNameAndPath(projectName); - - const projectDir = await createBareProject({ - projectName: appDir, - scopedAppName, - packages: usePackages, - noInstall, - force: cliOptions.force, - appRouter: cliOptions.appRouter, - }); - setImportAlias(projectDir, "@/"); - - // Write name to package.json - const pkgJson = fs.readJSONSync(path.join(projectDir, "package.json")) as ProofKitPackageJSON; - pkgJson.name = scopedAppName; - pkgJson.proofkitMetadata = { initVersion: getVersion() }; - - // ? Bun doesn't support this field (yet) - if (pkgManager !== "bun") { - const { stdout } = await execa(pkgManager, ["-v"], { - cwd: projectDir, - }); - pkgJson.packageManager = `${pkgManager}@${stdout.trim()}`; - } - - fs.writeJSONSync(path.join(projectDir, "package.json"), pkgJson, { - spaces: 2, - }); - - // Ensure proofkit.json exists with initial settings including ui - const initialSettings: Settings = - state.ui === "mantine" - ? { - appType: state.appType ?? "browser", - ui: "mantine", - auth: { type: "none" }, - envFile: ".env", - dataSources: [], - tanstackQuery: false, - replacedMainPage: false, - appliedUpgrades: [], - reactEmail: false, - reactEmailServer: false, - registryTemplates: [], - } - : { - appType: state.appType ?? "browser", - ui: "shadcn", - envFile: ".env", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], - }; - setSettings(initialSettings); - - // for webviewer apps FM is required, so don't ask - let dataSource = - state.appType === "webviewer" - ? (cliOptions.dataSource ?? (nonInteractive && !cliOptions.server ? "none" : "filemaker")) - : (cliOptions.dataSource ?? (nonInteractive ? "none" : undefined)); - if (!dataSource) { - dataSource = abortIfCancel( - await select({ - message: "Do you want to connect to a FileMaker Database now?", - options: [ - { - value: "filemaker", - label: "Yes", - hint: "Requires OttoFMS and Admin Server credentials", - }, - // { value: "supabase", label: "Supabase" }, - { - value: "none", - label: "No", - hint: "You'll be able to add a new data source later", - }, - ], - }), - ) as "filemaker" | "none" | "supabase"; - } - - if (dataSource === "filemaker") { - // later will split this flow to ask for which kind of data souce, but for now it's just FM - await promptForFileMakerDataSource({ - projectDir, - name: "filemaker", - adminApiKey: cliOptions.adminApiKey, - dataApiKey: cliOptions.dataApiKey, - server: cliOptions.server, - fileName: cliOptions.fileName, - layoutName: cliOptions.layoutName, - schemaName: cliOptions.schemaName, - }); - } else if (dataSource === "supabase") { - // TODO: add supabase - } - - await askForAuth({ projectDir }); - - if (!noInstall) { - await installDependencies({ projectDir }); - } - - if (dataSource === "filemaker") { - const shouldRunInitialCodegen = state.appType === "webviewer" && !(nonInteractive && !hasExplicitFileMakerInputs); - - if (shouldRunInitialCodegen) { - try { - await runCodegenCommand(); - } catch (error) { - throw createPostInitGenerationError({ - error, - appType: state.appType ?? "browser", - projectDir, - }); - } - } - } - - if (!noGit) { - await initializeGit(projectDir); - } - - logNextSteps({ - projectName: appDir, - noInstall, - }); -}; diff --git a/packages/cli-old/src/cli/menu.ts b/packages/cli-old/src/cli/menu.ts deleted file mode 100644 index be40d6a7..00000000 --- a/packages/cli-old/src/cli/menu.ts +++ /dev/null @@ -1,102 +0,0 @@ -import chalk from "chalk"; -import open from "open"; -import { confirm, log, select } from "~/cli/prompts.js"; - -import { DOCS_URL } from "~/consts.js"; -import { checkForAvailableUpgrades, runAllAvailableUpgrades } from "~/upgrades/index.js"; -import { getSettings } from "~/utils/parseSettings.js"; -import { runAdd } from "./add/index.js"; -import { runDeploy } from "./deploy/index.js"; -import { runRemove } from "./remove/index.js"; -import { runTypegen } from "./typegen/index.js"; -import { runUpgrade } from "./update/index.js"; -import { abortIfCancel } from "./utils.js"; - -export const runMenu = async () => { - const settings = getSettings(); - const upgrades = checkForAvailableUpgrades(); - - if (upgrades.length > 0) { - log.info( - `${chalk.yellow("There are upgrades available for your ProofKit project")}\n${upgrades - .map((upgrade) => `- ${upgrade.title}`) - .join("\n")}`, - ); - - const shouldRunUpgrades = abortIfCancel( - await confirm({ - message: "Would you like to run them now?", - initialValue: true, - }), - ); - - if (shouldRunUpgrades) { - await runAllAvailableUpgrades(); - log.success(chalk.green("Successfully ran all upgrades")); - } else { - log.info(`You can apply the upgrades later by running ${chalk.cyan("proofkit upgrade")}`); - } - } - - const menuChoice = abortIfCancel( - await select({ - message: "What would you like to do?", - options: [ - { - label: "Add Components", - value: "add", - hint: "Add new pages, schemas, data sources, etc.", - }, - { - label: "Remove Components", - value: "remove", - hint: "Remove pages, schemas, data sources, etc.", - }, - { - label: "Generate Types", - value: "typegen", - hint: "Update field definitions from your data sources", - }, - { - label: "Deploy", - value: "deploy", - hint: "Deploy your app to Vercel", - }, - { - label: "Upgrade Components", - value: "upgrade", - hint: "Update ProofKit components to latest version", - }, - { - label: "View Documentation", - value: "docs", - hint: "Open ProofKit documentation", - }, - ], - }), - ); - - switch (menuChoice) { - case "add": - await runAdd(undefined); - break; - case "remove": - await runRemove(undefined); - break; - case "docs": - log.info(`Opening ${chalk.cyan(DOCS_URL)} in your browser...`); - await open(DOCS_URL); - break; - case "typegen": - await runTypegen({ settings }); - break; - case "deploy": - await runDeploy(); - break; - case "upgrade": - await runUpgrade(); - break; - default: - throw new Error(`Unknown menu choice: ${menuChoice}`); - } -}; diff --git a/packages/cli-old/src/cli/ottofms.ts b/packages/cli-old/src/cli/ottofms.ts deleted file mode 100644 index 569ad348..00000000 --- a/packages/cli-old/src/cli/ottofms.ts +++ /dev/null @@ -1,268 +0,0 @@ -import axios, { AxiosError } from "axios"; -import chalk from "chalk"; -import open from "open"; -import randomstring from "randomstring"; -import { z } from "zod/v4"; - -import * as clack from "~/cli/prompts.js"; -import { abortIfCancel } from "./utils.js"; - -interface WizardResponse { - token: string; -} -export async function getOttoFMSToken({ url }: { url: URL }): Promise<{ token: string }> { - // generate a random string - const hash = randomstring.generate({ length: 18, charset: "alphanumeric" }); - - const loginUrl = new URL(`/otto/wizard/${hash}`, url.origin); - - const urlToOpen = loginUrl.toString(); - clack.log.info( - `${chalk.bold( - `If the browser window didn't open automatically, please open the following link to login into your OttoFMS server:`, - )}\n\n${chalk.cyan(urlToOpen)}`, - ); - - open(loginUrl.toString()).catch(() => { - // Ignore errors from open() - the user can manually open the URL - }); - - const loginSpinner = clack.spinner(); - - loginSpinner.start("Waiting for you to log in using the link above"); - - const data = await new Promise((resolve) => { - const pollingInterval = setInterval(() => { - axios - .get<{ response: WizardResponse }>(`${url.origin}/otto/api/cli/checkHash/${hash}`, { - headers: { - "Accept-Encoding": "deflate", - }, - }) - .then((result) => { - resolve(result.data.response); - clearTimeout(timeout); - clearInterval(pollingInterval); - axios - .delete(`${url.origin}/otto/api/cli/checkHash/${hash}`, { - headers: { - "Accept-Encoding": "deflate", - }, - }) - .catch(() => { - // Ignore cleanup errors - }); - }) - .catch(() => { - // noop - just try again - }); - }, 500); - - const timeout = setTimeout(() => { - clearInterval(pollingInterval); - loginSpinner.stop("Login timed out. No worries - it happens to the best of us."); - }, 180_000); // 3 minutes - }); - // clack.log.info(`Token: ${JSON.stringify(data)}`); - - loginSpinner.stop("Login complete."); - - return data; -} - -interface ListFilesResponse { - response: { - databases: { - clients: number; - decryptHint: string; - enabledExtPrivileges: string[]; - filename: string; - folder: string; - hasSavedDecryptKey: boolean; - id: string; - isEncrypted: boolean; - size: number; - status: string; - }[]; - }; -} - -export async function listFiles({ url, token }: { url: URL; token: string }) { - const response = await axios.get(`${url.origin}/otto/fmi/admin/api/v2/databases`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - return response.data.response.databases; -} - -interface ListAPIKeysResponse { - response: { - "api-keys": { - id: number; - key: string; - token: string; - user: string; - database: string; - label: string; - created_at: string; - updated_at: string; - }[]; - }; -} - -export async function listAPIKeys({ url, token }: { url: URL; token: string }) { - const response = await axios.get(`${url.origin}/otto/api/api-key`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - return response.data.response["api-keys"]; -} - -interface CreateAPIKeyResponse { - response: { - key: string; - token: string; - }; -} -export async function createDataAPIKey({ url, filename }: { url: URL; filename: string }) { - clack.log.info( - `${chalk.cyan("Creating a Data API Key")}\nEnter FileMaker credentials for ${chalk.bold(filename)}.\n${chalk.dim("The account must have the fmrest extended privilege enabled.")}`, - ); - - while (true) { - const username = abortIfCancel( - await clack.text({ - message: `Enter the account name for ${chalk.bold(filename)}`, - }), - ); - - const password = abortIfCancel( - await clack.password({ - message: `Enter the password for ${chalk.bold(username)}`, - }), - ); - - try { - const response = await createDataAPIKeyWithCredentials({ - url, - filename, - username, - password, - }); - - return response; - } catch (error) { - if (error instanceof AxiosError) { - const respMsg = - error.response?.data && "messages" in error.response.data - ? (error.response.data as { messages?: { text?: string }[] }).messages?.[0]?.text - : undefined; - - clack.log.error( - `${chalk.red("Error creating Data API key:")} ${respMsg ?? `Error code ${error.response?.status}`} -${chalk.dim( - error.response?.status === 400 && - `Common reasons this might happen: -- The provided credentials are incorrect. -- The account does not have the fmrest extended privilege enabled. - -You may also want to try to create an API directly in the OttoFMS dashboard: -${url.origin}/otto/app/api-keys`, -)} - `, - ); - } else { - clack.log.error(`${chalk.red("Error creating Data API key:")} Unknown error`); - } - const tryAgain = abortIfCancel( - await clack.confirm({ - message: "Do you want to try and enter credentials again?", - active: "Yes, try again", - inactive: "No, abort", - }), - ); - if (!tryAgain) { - throw new Error("User cancelled"); - } - } - } -} - -export async function createDataAPIKeyWithCredentials({ - url, - filename, - username, - password, -}: { - url: URL; - filename: string; - username: string; - password: string; -}) { - const response = await axios.post(`${url.origin}/otto/api/api-key/create-only`, { - database: filename, - label: "For FM Web App", - user: username, - pass: password, - }); - - return { apiKey: response.data.response.key }; -} - -export async function startDeployment({ payload, url, token }: { payload: unknown; url: URL; token: string }) { - const responseSchema = z.object({ - response: z.object({ - started: z.boolean(), - batchId: z.number(), - subDeploymentIds: z.array(z.number()), - }), - messages: z.array(z.object({ code: z.number(), text: z.string() })), - }); - - const response = await axios - .post(`${url.origin}/otto/api/deployment`, payload, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - .catch((error) => { - console.error(error.response.data); - throw error; - }); - - return responseSchema.parse(response.data); -} - -export async function getDeploymentStatus({ - url, - token, - deploymentId, -}: { - url: URL; - token: string; - deploymentId: number; -}) { - const schema = z.object({ - response: z.object({ - id: z.number(), - status: z.enum(["queued", "running", "scheduled", "complete", "aborted", "unknown"]), - running: z.coerce.boolean(), - created_at: z.string(), - started_at: z.string(), - updated_at: z.string(), - }), - messages: z.array(z.object({ code: z.number(), text: z.string() })), - }); - - const response = await axios.get(`${url.origin}/otto/api/deployment/${deploymentId}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - return schema.parse(response.data); -} diff --git a/packages/cli-old/src/cli/prompts.ts b/packages/cli-old/src/cli/prompts.ts deleted file mode 100644 index c4b7900e..00000000 --- a/packages/cli-old/src/cli/prompts.ts +++ /dev/null @@ -1,188 +0,0 @@ -import * as clack from "@clack/prompts"; -import { - checkbox as inquirerCheckbox, - confirm as inquirerConfirm, - input as inquirerInput, - password as inquirerPassword, - search as inquirerSearch, - select as inquirerSelect, -} from "@inquirer/prompts"; - -const CANCEL_SYMBOL = Symbol.for("@proofkit/cli/prompt-cancelled"); - -export const intro = clack.intro; -export const outro = clack.outro; -export const note = clack.note; -export const log = clack.log; -export const spinner = clack.spinner; -export const cancel = clack.cancel; - -export interface PromptOption { - value: T; - label: string; - hint?: string; - disabled?: boolean | string; -} - -export interface SearchPromptOption extends PromptOption { - keywords?: readonly string[]; -} - -function normalizeValidate( - validate: ((value: string) => string | undefined) | undefined, -): ((value: string) => string | boolean) | undefined { - if (!validate) { - return undefined; - } - - return (value: string) => validate(value) ?? true; -} - -function normalizeDisabledMessage(value: boolean | string | undefined) { - if (typeof value === "string") { - return value; - } - return value ? true : undefined; -} - -function isPromptCancel(error: unknown) { - return error instanceof Error && error.name === "ExitPromptError"; -} - -function withCancelSentinel(fn: () => Promise): Promise { - return fn().catch((error: unknown) => { - if (isPromptCancel(error)) { - return CANCEL_SYMBOL; - } - throw error; - }); -} - -export function isCancel(value: unknown): value is symbol { - return value === CANCEL_SYMBOL || clack.isCancel(value); -} - -function matchesSearch(option: SearchPromptOption, query: string) { - const haystack = [option.label, option.hint ?? "", ...(option.keywords ?? [])].join(" ").toLowerCase(); - return haystack.includes(query.trim().toLowerCase()); -} - -export function filterSearchOptions( - options: readonly SearchPromptOption[], - query: string | undefined, -) { - const term = query?.trim(); - if (!term) { - return options; - } - - return options.filter((option) => matchesSearch(option, term)); -} - -export function text(options: { - message: string; - defaultValue?: string; - placeholder?: string; - validate?: (value: string) => string | undefined; -}) { - return withCancelSentinel(() => - inquirerInput({ - message: options.message, - default: options.defaultValue, - validate: normalizeValidate(options.validate), - }), - ); -} - -export function password(options: { message: string; validate?: (value: string) => string | undefined }) { - return withCancelSentinel(() => - inquirerPassword({ - message: options.message, - validate: normalizeValidate(options.validate), - }), - ); -} - -export function confirm(options: { message: string; initialValue?: boolean; active?: string; inactive?: string }) { - return withCancelSentinel( - () => - inquirerConfirm({ - message: options.message, - default: options.initialValue, - }) as Promise, - ); -} - -export function select(options: { - message: string; - options: PromptOption[]; - maxItems?: number; - initialValue?: T; -}) { - return withCancelSentinel(() => - inquirerSelect({ - message: options.message, - pageSize: options.maxItems ?? 10, - default: options.initialValue, - choices: options.options.map((option) => ({ - value: option.value, - name: option.label, - description: option.hint, - disabled: normalizeDisabledMessage(option.disabled), - })), - }), - ); -} - -export function searchSelect(options: { - message: string; - searchLabel?: string; - emptyMessage?: string; - options: SearchPromptOption[]; -}) { - return withCancelSentinel(() => - inquirerSearch({ - message: options.message, - pageSize: 10, - source: (input) => { - const filtered = filterSearchOptions(options.options, input); - if (filtered.length === 0) { - return [ - { - value: "__no_matches__" as T, - name: options.emptyMessage ?? "No matches found. Keep typing to refine your search.", - disabled: options.emptyMessage ?? "No matches found", - }, - ]; - } - - return filtered.map((option) => ({ - value: option.value, - name: option.label, - description: option.hint, - disabled: normalizeDisabledMessage(option.disabled), - })); - }, - }), - ); -} - -export function multiSearchSelect(options: { - message: string; - options: SearchPromptOption[]; - required?: boolean; -}) { - return withCancelSentinel(() => - inquirerCheckbox({ - message: options.message, - pageSize: 10, - required: options.required, - choices: options.options.map((option) => ({ - value: option.value, - name: option.label, - description: option.hint, - disabled: normalizeDisabledMessage(option.disabled), - })), - }), - ); -} diff --git a/packages/cli-old/src/cli/react-email.ts b/packages/cli-old/src/cli/react-email.ts deleted file mode 100644 index f0ac245a..00000000 --- a/packages/cli-old/src/cli/react-email.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Command, Option } from "commander"; -import * as p from "~/cli/prompts.js"; - -import { installReactEmail } from "~/installers/react-email.js"; - -export const runAddReactEmailCommand = async ({ - noInstall, - installServerFiles, -}: { - noInstall?: boolean; - installServerFiles?: boolean; -} = {}) => { - const spinner = p.spinner(); - spinner.start("Adding React Email"); - await installReactEmail({ noInstall, installServerFiles }); - spinner.stop("React Email added"); -}; - -export const makeAddReactEmailCommand = () => { - const addReactEmailCommand = new Command("react-email") - .description("Add React Email scaffolding to your project") - .addOption(new Option("--noInstall", "Do not run your package manager install command").default(false)) - .option("--installServerFiles", "Also scaffold provider-specific server email files", false) - .action((args: { noInstall?: boolean; installServerFiles?: boolean }) => runAddReactEmailCommand(args)); - - return addReactEmailCommand; -}; diff --git a/packages/cli-old/src/cli/remove/data-source.ts b/packages/cli-old/src/cli/remove/data-source.ts deleted file mode 100644 index dbc6aebf..00000000 --- a/packages/cli-old/src/cli/remove/data-source.ts +++ /dev/null @@ -1,153 +0,0 @@ -import path from "node:path"; -import { Command } from "commander"; -import dotenv from "dotenv"; -import fs from "fs-extra"; -import { z } from "zod/v4"; -import * as p from "~/cli/prompts.js"; - -import { removeFromFmschemaConfig, runCodegenCommand } from "~/generators/fmdapi.js"; -import { ciOption, debugOption, nonInteractiveOption } from "~/globalOptions.js"; -import { initProgramState, isNonInteractiveMode, state } from "~/state.js"; -import { type DataSource, getSettings, setSettings } from "~/utils/parseSettings.js"; -import { abortIfCancel, ensureProofKitProject, UserAbortedError } from "../utils.js"; - -function getDataSourceInfo(source: DataSource) { - if (source.type !== "fm") { - return source.type; - } - - const envFile = path.join(state.projectDir, ".env"); - if (fs.existsSync(envFile)) { - dotenv.config({ path: envFile }); - } - - const server = process.env[source.envNames.server] || "unknown server"; - const database = process.env[source.envNames.database] || "unknown database"; - - try { - // Format the server URL to be more readable - const serverUrl = new URL(server); - const formattedServer = serverUrl.hostname; - return `${formattedServer}/${database}`; - } catch (error) { - if (state.debug) { - console.error("Error parsing server URL:", error); - } - return `${server}/${database}`; - } -} - -export const runRemoveDataSourceCommand = async (name?: string) => { - const settings = getSettings(); - - if (settings.dataSources.length === 0) { - p.note("No data sources found in your project."); - return; - } - - let dataSourceName = name; - - // If no name provided, prompt for selection - if (dataSourceName) { - // Validate that the provided name exists - const dataSourceExists = settings.dataSources.some((source) => source.name === dataSourceName); - if (!dataSourceExists) { - throw new Error(`Data source "${dataSourceName}" not found in your project.`); - } - } else { - dataSourceName = abortIfCancel( - await p.select({ - message: "Which data source do you want to remove?", - options: settings.dataSources.map((source) => { - let info = ""; - try { - info = getDataSourceInfo(source); - } catch (error) { - if (state.debug) { - console.error("Error getting data source info:", error); - } - info = "unknown connection"; - } - return { - label: `${source.name} (${info})`, - value: source.name, - }; - }), - }), - ); - } - - let confirmed = true; - if (!isNonInteractiveMode()) { - confirmed = abortIfCancel( - await p.confirm({ - message: `Are you sure you want to remove the data source "${dataSourceName}"? This will only remove it from your configuration, not replace any possible usage, which may cause TypeScript errors.`, - }), - ); - - if (!confirmed) { - throw new UserAbortedError(); - } - } - - // Get the data source before removing it - const dataSource = settings.dataSources.find((source) => source.name === dataSourceName); - - // Remove the data source from settings - settings.dataSources = settings.dataSources.filter((source) => source.name !== dataSourceName); - - // Save the updated settings - setSettings(settings); - - if (dataSource?.type === "fm") { - // For FileMaker data sources, remove from fmschema.config.mjs - removeFromFmschemaConfig({ - dataSourceName, - }); - - if (state.debug) { - p.note("Removed schemas from fmschema.config.mjs"); - } - - // Remove the schema folder for this data source - const schemaFolderPath = path.join(state.projectDir, "src", "config", "schemas", dataSourceName); - if (fs.existsSync(schemaFolderPath)) { - fs.removeSync(schemaFolderPath); - if (state.debug) { - p.note(`Removed schema folder at ${schemaFolderPath}`); - } - } - - // Run typegen to regenerate types - await runCodegenCommand(); - if (state.debug) { - p.note("Successfully regenerated types"); - } - } - - p.note(`Successfully removed data source "${dataSourceName}"`); -}; - -export const makeRemoveDataSourceCommand = () => { - const removeDataSourceCommand = new Command("data") - .description("Remove a data source from your project") - .option("--name ", "Name of the data source to remove") - .addOption(ciOption) - .addOption(nonInteractiveOption) - .addOption(debugOption) - .action(async (options) => { - const schema = z.object({ - name: z.string().optional(), - }); - const validated = schema.parse(options); - await runRemoveDataSourceCommand(validated.name); - }); - - removeDataSourceCommand.hook("preAction", (_thisCommand, actionCommand) => { - initProgramState(actionCommand.opts()); - state.baseCommand = "remove"; - ensureProofKitProject({ commandName: "remove" }); - }); - - return removeDataSourceCommand; -}; diff --git a/packages/cli-old/src/cli/remove/index.ts b/packages/cli-old/src/cli/remove/index.ts deleted file mode 100644 index 954e8765..00000000 --- a/packages/cli-old/src/cli/remove/index.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Command } from "commander"; -import * as p from "~/cli/prompts.js"; - -import { ciOption, debugOption } from "~/globalOptions.js"; -import { initProgramState, state } from "~/state.js"; -import { getSettings } from "~/utils/parseSettings.js"; -import { abortIfCancel, ensureProofKitProject } from "../utils.js"; -import { makeRemoveDataSourceCommand, runRemoveDataSourceCommand } from "./data-source.js"; -import { makeRemovePageCommand, runRemovePageAction } from "./page.js"; -import { makeRemoveSchemaCommand, runRemoveSchemaAction } from "./schema.js"; - -export const runRemove = async (_name: string | undefined) => { - const settings = getSettings(); - - const removeType = abortIfCancel( - await p.select({ - message: "What do you want to remove from your project?", - options: [ - { label: "Page", value: "page" }, - { - label: "Schema", - value: "schema", - hint: "remove a table or layout schema", - }, - ...(settings.appType === "browser" - ? [ - { - label: "Data Source", - value: "data", - hint: "remove a database or FileMaker connection", - }, - ] - : []), - ], - }), - ); - - if (removeType === "data") { - await runRemoveDataSourceCommand(); - } else if (removeType === "page") { - await runRemovePageAction(); - } else if (removeType === "schema") { - await runRemoveSchemaAction(); - } -}; - -export function makeRemoveCommand() { - const removeCommand = new Command("remove") - .description("Remove a component from your project") - .argument("[name]", "Type of component to remove") - .addOption(ciOption) - .addOption(debugOption) - .action(runRemove); - - removeCommand.hook("preAction", (_thisCommand, _actionCommand) => { - initProgramState(_actionCommand.opts()); - state.baseCommand = "remove"; - ensureProofKitProject({ commandName: "remove" }); - }); - removeCommand.hook("preSubcommand", (_thisCommand, _subCommand) => { - initProgramState(_subCommand.opts()); - state.baseCommand = "remove"; - ensureProofKitProject({ commandName: "remove" }); - }); - - // Add subcommands - removeCommand.addCommand(makeRemoveDataSourceCommand()); - removeCommand.addCommand(makeRemovePageCommand()); - removeCommand.addCommand(makeRemoveSchemaCommand()); - - return removeCommand; -} diff --git a/packages/cli-old/src/cli/remove/page.ts b/packages/cli-old/src/cli/remove/page.ts deleted file mode 100644 index d6574589..00000000 --- a/packages/cli-old/src/cli/remove/page.ts +++ /dev/null @@ -1,214 +0,0 @@ -import path from "node:path"; -import { Command } from "commander"; -import fs from "fs-extra"; -import { Node, type Project, type PropertyAssignment, SyntaxKind } from "ts-morph"; -import * as p from "~/cli/prompts.js"; - -import { ciOption, debugOption } from "~/globalOptions.js"; -import { initProgramState, state } from "~/state.js"; -import { getSettings } from "~/utils/parseSettings.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; -import { abortIfCancel, ensureProofKitProject } from "../utils.js"; - -const getExistingRoutes = (project: Project): { label: string; href: string }[] => { - const navFilePath = path.join(state.projectDir, "src/app/navigation.tsx"); - - // If navigation file doesn't exist (e.g., webviewer apps), there are no nav routes to remove - if (!fs.existsSync(navFilePath)) { - return []; - } - - const sourceFile = project.addSourceFileAtPath(navFilePath); - - const routes: { label: string; href: string }[] = []; - - // Get primary routes - const primaryRoutes = sourceFile - .getVariableDeclaration("primaryRoutes") - ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression) - ?.getElements(); - - if (primaryRoutes) { - for (const element of primaryRoutes) { - if (Node.isObjectLiteralExpression(element)) { - const labelProp = element - .getProperties() - .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "label"); - const hrefProp = element - .getProperties() - .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "href"); - - const label = labelProp?.getInitializer()?.getText().replace(/['"]/g, ""); - const href = hrefProp?.getInitializer()?.getText().replace(/['"]/g, ""); - - if (label && href) { - routes.push({ label, href }); - } - } - } - } - - // Get secondary routes - const secondaryRoutes = sourceFile - .getVariableDeclaration("secondaryRoutes") - ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression) - ?.getElements(); - - if (secondaryRoutes) { - for (const element of secondaryRoutes) { - if (Node.isObjectLiteralExpression(element)) { - const labelProp = element - .getProperties() - .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "label"); - const hrefProp = element - .getProperties() - .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "href"); - - const label = labelProp?.getInitializer()?.getText().replace(/['"]/g, ""); - const href = hrefProp?.getInitializer()?.getText().replace(/['"]/g, ""); - - if (label && href) { - routes.push({ label, href }); - } - } - } - } - - return routes; -}; - -const removeRouteFromNav = async (project: Project, routeToRemove: string) => { - const navFilePath = path.join(state.projectDir, "src/app/navigation.tsx"); - - // Skip if there is no navigation file - if (!fs.existsSync(navFilePath)) { - return; - } - - const sourceFile = project.addSourceFileAtPath(navFilePath); - - // Remove from primary routes - const primaryRoutes = sourceFile - .getVariableDeclaration("primaryRoutes") - ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression); - - if (primaryRoutes) { - const elements = primaryRoutes.getElements(); - for (let i = elements.length - 1; i >= 0; i--) { - const element = elements[i]; - if (Node.isObjectLiteralExpression(element)) { - const hrefProp = element - .getProperties() - .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "href"); - - const href = hrefProp?.getInitializer()?.getText().replace(/['"]/g, ""); - - if (href === routeToRemove) { - primaryRoutes.removeElement(i); - } - } - } - } - - // Remove from secondary routes - const secondaryRoutes = sourceFile - .getVariableDeclaration("secondaryRoutes") - ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression); - - if (secondaryRoutes) { - const elements = secondaryRoutes.getElements(); - for (let i = elements.length - 1; i >= 0; i--) { - const element = elements[i]; - if (Node.isObjectLiteralExpression(element)) { - const hrefProp = element - .getProperties() - .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "href"); - - const href = hrefProp?.getInitializer()?.getText().replace(/['"]/g, ""); - - if (href === routeToRemove) { - secondaryRoutes.removeElement(i); - } - } - } - } - - await formatAndSaveSourceFiles(project); -}; - -export const runRemovePageAction = async (routeName?: string) => { - const _settings = getSettings(); - const projectDir = state.projectDir; - const project = getNewProject(projectDir); - - // Get existing routes - const routes = getExistingRoutes(project); - - if (routes.length === 0) { - return p.cancel("No pages found in the navigation."); - } - - let selectedRouteName = routeName; - if (!selectedRouteName) { - selectedRouteName = abortIfCancel( - await p.select({ - message: "Select the page to remove", - options: routes.map((route) => ({ - label: `${route.label} (${route.href})`, - value: route.href, - })), - }), - ); - } - - if (!selectedRouteName.startsWith("/")) { - selectedRouteName = `/${selectedRouteName}`; - } - - const pagePath = - state.appType === "browser" - ? path.join(projectDir, "src/app/(main)", selectedRouteName) - : path.join(projectDir, "src/routes", selectedRouteName); - - const spinner = p.spinner(); - spinner.start("Removing page"); - - try { - // Check if directory exists - if (!fs.existsSync(pagePath)) { - spinner.stop("Page not found!"); - return p.cancel(`Page at ${selectedRouteName} does not exist`); - } - - // Remove from navigation first (if present) - await removeRouteFromNav(project, selectedRouteName); - - // Remove the page directory - await fs.remove(pagePath); - - spinner.stop("Page removed successfully!"); - } catch (error) { - spinner.stop("Failed to remove page!"); - console.error("Error removing page:", error); - process.exit(1); - } -}; - -export const makeRemovePageCommand = () => { - const removePageCommand = new Command("page") - .description("Remove a page from your project") - .argument("[route]", "The route of the page to remove") - .addOption(ciOption) - .addOption(debugOption) - .action(async (route: string) => { - await runRemovePageAction(route); - }); - - removePageCommand.hook("preAction", (_thisCommand, actionCommand) => { - initProgramState(actionCommand.opts()); - state.baseCommand = "remove"; - ensureProofKitProject({ commandName: "remove" }); - }); - - return removePageCommand; -}; diff --git a/packages/cli-old/src/cli/remove/schema.ts b/packages/cli-old/src/cli/remove/schema.ts deleted file mode 100644 index 4cc40088..00000000 --- a/packages/cli-old/src/cli/remove/schema.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Command } from "commander"; -import { z } from "zod/v4"; -import * as p from "~/cli/prompts.js"; - -import { getExistingSchemas, removeLayout } from "~/generators/fmdapi.js"; -import { state } from "~/state.js"; -import { getSettings, type Settings } from "~/utils/parseSettings.js"; -import { abortIfCancel } from "../utils.js"; - -export const runRemoveSchemaAction = async (opts?: { - projectDir?: string; - settings?: Settings; - sourceName?: string; - schemaName?: string; -}) => { - const settings = opts?.settings ?? getSettings(); - const projectDir = opts?.projectDir ?? state.projectDir; - let sourceName = opts?.sourceName; - - // If there is more than one fm data source, prompt for which one to remove from - if (!sourceName && settings.dataSources.filter((s) => s.type === "fm").length > 1) { - const dataSourceName = await p.select({ - message: "Which FileMaker data source do you want to remove a layout from?", - options: settings.dataSources.filter((s) => s.type === "fm").map((s) => ({ label: s.name, value: s.name })), - }); - if (p.isCancel(dataSourceName)) { - p.cancel(); - process.exit(0); - } - sourceName = z.string().parse(dataSourceName); - } - - if (!sourceName) { - sourceName = "filemaker"; - } - - const dataSource = settings.dataSources.filter((s) => s.type === "fm").find((s) => s.name === sourceName); - if (!dataSource) { - throw new Error(`FileMaker data source ${sourceName} not found in your ProofKit config`); - } - - // Get existing schemas for this data source - const existingSchemas = getExistingSchemas({ - projectDir, - dataSourceName: sourceName, - }); - - if (existingSchemas.length === 0) { - p.note(`No layouts found in data source "${sourceName}"`, "Nothing to remove"); - return; - } - - // Show existing schemas and let user pick one to remove - const schemaToRemove = - opts?.schemaName ?? - abortIfCancel( - await p.select({ - message: "Select a layout to remove", - options: existingSchemas - .map((schema) => ({ - label: `${schema.layout} (${schema.schemaName})`, - value: schema.schemaName ?? "", - })) - .filter((opt) => opt.value !== ""), - }), - ); - - // Confirm removal - const confirmRemoval = await p.confirm({ - message: `Are you sure you want to remove the layout "${schemaToRemove}"?`, - initialValue: false, - }); - - if (p.isCancel(confirmRemoval) || !confirmRemoval) { - p.cancel("Operation cancelled"); - process.exit(0); - } - - // Remove the schema - await removeLayout({ - projectDir, - dataSourceName: sourceName, - schemaName: schemaToRemove, - runCodegen: true, - }); - - p.outro(`Layout "${schemaToRemove}" has been removed from your project`); -}; - -export const makeRemoveSchemaCommand = () => { - const removeSchemaCommand = new Command("layout") - .alias("schema") - .description("Remove a layout from your fmschema file") - .action(async (opts: { settings: Settings }) => { - const settings = opts.settings; - await runRemoveSchemaAction({ settings }); - }); - - return removeSchemaCommand; -}; diff --git a/packages/cli-old/src/cli/tanstack-query.ts b/packages/cli-old/src/cli/tanstack-query.ts deleted file mode 100644 index fb29fac0..00000000 --- a/packages/cli-old/src/cli/tanstack-query.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Command } from "commander"; -import * as p from "~/cli/prompts.js"; - -import { injectTanstackQuery } from "~/generators/tanstack-query.js"; - -export const runAddTanstackQueryCommand = async () => { - const spinner = p.spinner(); - spinner.start("Adding Tanstack Query"); - await injectTanstackQuery(); - spinner.stop("Tanstack Query added"); -}; - -export const makeAddTanstackQueryCommand = () => { - const addTanstackQueryCommand = new Command("tanstack-query") - .description("Add Tanstack Query to your project") - .action(runAddTanstackQueryCommand); - - return addTanstackQueryCommand; -}; diff --git a/packages/cli-old/src/cli/typegen/index.ts b/packages/cli-old/src/cli/typegen/index.ts deleted file mode 100644 index 23a4f61c..00000000 --- a/packages/cli-old/src/cli/typegen/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Command } from "commander"; - -import { runCodegenCommand } from "~/generators/fmdapi.js"; -import type { Settings } from "~/utils/parseSettings.js"; -import { ensureProofKitProject } from "../utils.js"; - -export async function runTypegen(_opts: { settings: Settings }) { - await runCodegenCommand(); -} - -export const makeTypegenCommand = () => { - const typegenCommand = new Command("typegen").description("Generate types for your project").action(runTypegen); - - typegenCommand.hook("preAction", (_thisCommand, actionCommand) => { - const settings = ensureProofKitProject({ commandName: "typegen" }); - actionCommand.setOptionValue("settings", settings); - }); - - return typegenCommand; -}; diff --git a/packages/cli-old/src/cli/update/index.ts b/packages/cli-old/src/cli/update/index.ts deleted file mode 100644 index 93eca92b..00000000 --- a/packages/cli-old/src/cli/update/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import chalk from "chalk"; -import { Command } from "commander"; - -import { initProgramState, state } from "~/state.js"; -import { runAllAvailableUpgrades } from "~/upgrades/index.js"; -import { logger } from "~/utils/logger.js"; -import { ensureProofKitProject } from "../utils.js"; - -export const runUpgrade = async () => { - initProgramState({}); - state.baseCommand = "upgrade"; - ensureProofKitProject({ commandName: "upgrade" }); - - logger.info("\nUpgrading ProofKit components...\n"); - - try { - await runAllAvailableUpgrades(); - logger.info(chalk.green("โœ” Successfully upgraded components\n")); - } catch (error) { - logger.error("Failed to upgrade components:", error); - process.exit(1); - } -}; - -export const upgrade = new Command() - .name("upgrade") - .description("Upgrade ProofKit components in your project") - .action(runUpgrade); diff --git a/packages/cli-old/src/cli/update/makeUpgradeCommand.ts b/packages/cli-old/src/cli/update/makeUpgradeCommand.ts deleted file mode 100644 index 8232b3fe..00000000 --- a/packages/cli-old/src/cli/update/makeUpgradeCommand.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Command } from "commander"; - -import { ciOption } from "~/globalOptions.js"; -import { initProgramState, state } from "~/state.js"; -import { ensureProofKitProject } from "../utils.js"; -import { runUpgrade } from "./index.js"; - -export const makeUpgradeCommand = () => { - const upgradeCommand = new Command("upgrade") - .description("Upgrade ProofKit components in your project") - .addOption(ciOption) - .action(async (args) => { - initProgramState(args); - - await runUpgrade(); - }); - - upgradeCommand.hook("preAction", (_thisCommand, _actionCommand) => { - initProgramState(_actionCommand.opts()); - state.baseCommand = "upgrade"; - ensureProofKitProject({ commandName: "upgrade" }); - }); - - return upgradeCommand; -}; diff --git a/packages/cli-old/src/cli/utils.ts b/packages/cli-old/src/cli/utils.ts deleted file mode 100644 index 37a6897f..00000000 --- a/packages/cli-old/src/cli/utils.ts +++ /dev/null @@ -1,49 +0,0 @@ -import path from "node:path"; -import chalk from "chalk"; -import fs from "fs-extra"; -import z, { ZodError } from "zod/v4"; - -import { cancel, isCancel } from "~/cli/prompts.js"; -import { npmName } from "~/consts.js"; -import { getSettings } from "~/utils/parseSettings.js"; - -/** - * Runs before any add command is run. Checks if the user is in a ProofKit project and if the - * proofkit.json file is valid. - */ -export const ensureProofKitProject = ({ commandName }: { commandName: string }) => { - const settingsExists = fs.existsSync(path.join(process.cwd(), "proofkit.json")); - if (!settingsExists) { - console.log( - chalk.yellow( - `The "${commandName}" command requires an existing ProofKit project. -Please run " ${npmName} init" first, or try this command again when inside a ProofKit project.`, - ), - ); - process.exit(1); - } - - try { - return getSettings(); - } catch (error) { - console.log(chalk.red("Error parsing ProofKit settings file:")); - if (error instanceof ZodError) { - console.log(z.prettifyError(error)); - } else { - console.log(error); - } - - process.exit(1); - } -}; - -export class UserAbortedError extends Error {} -export function abortIfCancel(value: symbol | string): string; -export function abortIfCancel(value: symbol | T): T; -export function abortIfCancel(value: T | symbol): T { - if (isCancel(value)) { - cancel(); - throw new UserAbortedError(); - } - return value; -} diff --git a/packages/cli-old/src/consts.ts b/packages/cli-old/src/consts.ts deleted file mode 100644 index 45a00383..00000000 --- a/packages/cli-old/src/consts.ts +++ /dev/null @@ -1,35 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -import { getVersion } from "./utils/getProofKitVersion.js"; - -// Path is in relation to a single index.js file inside ./dist -const __filename = fileURLToPath(import.meta.url); -const distPath = path.dirname(__filename); -export const PKG_ROOT = path.join(distPath, "../"); -export const cliName = "proofkit"; -export const npmName = "@proofkit/cli"; -export const DOCS_URL = "https://proofkit.proof.sh"; - -const version = getVersion(); -const versionCharLength = version.length; -//export const PKG_ROOT = path.dirname(require.main.filename); - -export const TITLE_TEXT = ` - _______ ___ ___ ____ _ _ -|_ __ \\ .' ..]|_ ||_ _| (_) / |_ - | |__) |_ .--. .--. .--. _| |_ | |_/ / __ \`| |-' - | ___/[ \`/'\`\\]/ .'\`\\ \\/ .'\`\\ \\'-| |-' | __'. [ | | | - _| |_ | | | \\__. || \\__. | | | _| | \\ \\_ | | | |, -|_____| [___] '.__.' '.__.' [___] |____||____|[___]\\__/ -${" ".repeat(61 - versionCharLength)}v${version} -`; -export const DEFAULT_APP_NAME = "my-proofkit-app"; -export const CREATE_FM_APP = cliName; - -// Registry URL is injected at build time via tsdown define -declare const __REGISTRY_URL__: string; -// Provide a safe fallback when running from source (not built) -export const DEFAULT_REGISTRY_URL = - // typeof check avoids ReferenceError if not defined at runtime - typeof __REGISTRY_URL__ !== "undefined" && __REGISTRY_URL__ ? __REGISTRY_URL__ : "https://proofkit.proof.sh"; diff --git a/packages/cli-old/src/generators/auth.ts b/packages/cli-old/src/generators/auth.ts deleted file mode 100644 index 3ecd4080..00000000 --- a/packages/cli-old/src/generators/auth.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { readFileSync, writeFileSync } from "node:fs"; -import path from "node:path"; -import { glob } from "glob"; - -import { installDependencies } from "~/helpers/installDependencies.js"; -import { betterAuthInstaller } from "~/installers/better-auth.js"; -import { clerkInstaller } from "~/installers/clerk.js"; -import { proofkitAuthInstaller } from "~/installers/proofkit-auth.js"; -import { state } from "~/state.js"; -import { getSettings, mergeSettings } from "~/utils/parseSettings.js"; - -export async function addAuth({ - options, - noInstall = false, - projectDir = process.cwd(), -}: { - options: - | { type: "clerk" } - | { - type: "fmaddon"; - emailProvider?: "plunk" | "resend"; - apiKey?: string; - } - | { type: "better-auth" }; - projectDir?: string; - noInstall?: boolean; -}) { - const settings = getSettings(); - if (settings.ui === "shadcn") { - throw new Error("Shadcn projects should add auth using the template registry"); - } - if (settings.auth.type !== "none") { - throw new Error("Auth already exists"); - } - if (!settings.dataSources.some((o) => o.type === "fm") && options.type === "fmaddon") { - throw new Error("A FileMaker data source is required to use the FM Add-on Auth"); - } - if (!settings.dataSources.some((o) => o.type === "fm") && options.type === "better-auth") { - throw new Error("A FileMaker data source is required to use the Better-Auth"); - } - - if (options.type === "clerk") { - await addClerkAuth({ projectDir }); - } else if (options.type === "fmaddon") { - await addFmaddonAuth(); - } - - // Replace actionClient with authedActionClient in all action files - await replaceActionClientWithAuthed(); - - if (!noInstall) { - await installDependencies({ projectDir }); - } -} - -async function addClerkAuth({ projectDir = process.cwd() }: { projectDir?: string }) { - await clerkInstaller({ projectDir }); - mergeSettings({ auth: { type: "clerk" } }); -} - -async function addFmaddonAuth() { - await proofkitAuthInstaller(); - mergeSettings({ auth: { type: "fmaddon" } }); -} - -async function replaceActionClientWithAuthed() { - const projectDir = state.projectDir; - const actionFiles = await glob("src/app/(main)/**/actions.ts", { - cwd: projectDir, - }); - - for (const file of actionFiles) { - const fullPath = path.join(projectDir, file); - const content = readFileSync(fullPath, "utf-8"); - const updatedContent = content.replace(/actionClient/g, "authedActionClient"); - writeFileSync(fullPath, updatedContent); - } -} - -async function _addBetterAuth() { - await betterAuthInstaller(); - mergeSettings({ auth: { type: "better-auth" } }); -} diff --git a/packages/cli-old/src/generators/fmdapi.ts b/packages/cli-old/src/generators/fmdapi.ts deleted file mode 100644 index 27e54ab0..00000000 --- a/packages/cli-old/src/generators/fmdapi.ts +++ /dev/null @@ -1,525 +0,0 @@ -import path from "node:path"; -import { generateTypedClients } from "@proofkit/typegen"; -import type { typegenConfigSingle } from "@proofkit/typegen/config"; -import { config as dotenvConfig } from "dotenv"; -import fs from "fs-extra"; -import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser"; -import { SyntaxKind } from "ts-morph"; -import type { z } from "zod/v4"; - -import { state } from "~/state.js"; -import { logger } from "~/utils/logger.js"; -import type { envNamesSchema } from "~/utils/parseSettings.js"; -import { getNewProject } from "~/utils/ts-morph.js"; - -// Input schema for functions like addLayout -// This might be different from the layout config stored in the file -interface Schema { - layoutName: string; - schemaName: string; - valueLists?: "strict" | "allowEmpty" | "ignore"; - generateClient?: boolean; - strictNumbers?: boolean; -} - -// For any data source configuration object (fmdapi or fmodata) -type AnyDataSourceConfig = z.infer; -// For a single fmdapi data source configuration object -type FmdapiDataSourceConfig = Extract; -// For a single layout configuration object within a data source -type ImportedLayoutConfig = FmdapiDataSourceConfig["layouts"][number]; - -// This type represents the actual structure of the JSONC file, including $schema -interface FullProofkitTypegenJsonFile { - $schema?: string; - config: AnyDataSourceConfig | AnyDataSourceConfig[]; -} - -const typegenConfigFileName = "proofkit-typegen.config.jsonc"; - -// Helper function to normalize data sources by adding default type for backwards compatibility -// This mirrors the zod preprocess in @proofkit/typegen that defaults type to "fmdapi" -function normalizeDataSource(ds: AnyDataSourceConfig): AnyDataSourceConfig { - if (!("type" in ds) || ds.type === undefined) { - return { ...(ds as object), type: "fmdapi" } as AnyDataSourceConfig; - } - return ds; -} - -function normalizeConfig( - config: AnyDataSourceConfig | AnyDataSourceConfig[], -): AnyDataSourceConfig | AnyDataSourceConfig[] { - if (Array.isArray(config)) { - return config.map(normalizeDataSource); - } - return normalizeDataSource(config); -} - -// Helper functions for JSON config -async function readJsonConfigFile(configPath: string): Promise { - if (!fs.existsSync(configPath)) { - return null; - } - try { - const fileContent = await fs.readFile(configPath, "utf8"); - const parsed = parseJsonc(fileContent) as FullProofkitTypegenJsonFile; - // Normalize config to add default type for backwards compatibility - if (parsed.config) { - parsed.config = normalizeConfig(parsed.config); - } - return parsed; - } catch (error) { - console.error(`Error reading or parsing JSONC config at ${configPath}:`, error); - // Return a default structure for the *file* if parsing fails but file exists - return { - $schema: "https://proofkit.proof.sh/typegen-config-schema.json", - config: [], - }; - } -} - -async function writeJsonConfigFile(configPath: string, fileContent: FullProofkitTypegenJsonFile) { - // Check if file exists to preserve comments - if (fs.existsSync(configPath)) { - const originalText = await fs.readFile(configPath, "utf8"); - // Use jsonc-parser's modify function to preserve comments - const edits = modify(originalText, ["config"], fileContent.config, { - formattingOptions: { - tabSize: 2, - insertSpaces: true, - eol: "\n", - }, - }); - const modifiedText = applyEdits(originalText, edits); - await fs.writeFile(configPath, modifiedText, "utf8"); - } else { - // If file doesn't exist, create it with proper formatting - await fs.writeJson(configPath, fileContent, { spaces: 2 }); - } -} - -export async function addLayout({ - projectDir = process.cwd(), - schemas, - runCodegen = true, - dataSourceName, -}: { - projectDir?: string; - schemas: Schema[]; - runCodegen?: boolean; - dataSourceName: string; -}) { - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - let fileContent = await readJsonConfigFile(jsonConfigPath); - - if (!fileContent) { - fileContent = { - $schema: "https://proofkit.proof.sh/typegen-config-schema.json", - config: [], - }; - } - - // Work with the 'config' property which is TypegenConfig['config'] - const configProperty = fileContent.config; - - let configArray: AnyDataSourceConfig[]; - if (Array.isArray(configProperty)) { - configArray = configProperty; - } else { - configArray = [configProperty]; - fileContent.config = configArray; // Update fileContent to ensure it's an array for later ops - } - - const layoutsToAdd: ImportedLayoutConfig[] = schemas.map((schema) => ({ - layoutName: schema.layoutName, - schemaName: schema.schemaName, - valueLists: schema.valueLists, - generateClient: schema.generateClient, - strictNumbers: schema.strictNumbers, - })); - - let targetDataSource: FmdapiDataSourceConfig | undefined = configArray.find( - (ds): ds is FmdapiDataSourceConfig => - ds.type === "fmdapi" && - (ds.path?.endsWith(dataSourceName) || ds.path?.endsWith(`${dataSourceName}/`) || ds.path === dataSourceName), - ); - - if (targetDataSource) { - targetDataSource.layouts = targetDataSource.layouts || []; - } else { - targetDataSource = { - type: "fmdapi", - layouts: [], - path: `./src/config/schemas/${dataSourceName}`, - // other default properties for a new DataSourceConfig can be added here if needed - envNames: undefined, - }; - configArray.push(targetDataSource); - } - - targetDataSource.layouts.push(...layoutsToAdd); - // fileContent.config is already pointing to configArray if it was modified - - await writeJsonConfigFile(jsonConfigPath, fileContent); - - if (runCodegen) { - await runCodegenCommand(); - } -} - -export async function addConfig({ - config, - projectDir, - runCodegen = true, -}: { - config: FmdapiDataSourceConfig | FmdapiDataSourceConfig[]; - projectDir: string; - runCodegen?: boolean; -}) { - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - let fileContent = await readJsonConfigFile(jsonConfigPath); - - const configsToAdd = Array.isArray(config) ? config : [config]; - - if (fileContent) { - if (Array.isArray(fileContent.config)) { - fileContent.config.push(...configsToAdd); - } else { - fileContent.config = [fileContent.config, ...configsToAdd]; - } - } else { - fileContent = { - $schema: "https://proofkit.proof.sh/typegen-config-schema.json", - config: configsToAdd, - }; - } - - await writeJsonConfigFile(jsonConfigPath, fileContent); - - if (runCodegen) { - await runCodegenCommand(); - } -} - -export async function ensureWebviewerFmMcpConfig({ - projectDir, - connectedFileName, - dataSourceName = "filemaker", - baseUrl, -}: { - projectDir: string; - connectedFileName?: string; - dataSourceName?: string; - baseUrl?: string; -}) { - const newConfig: FmdapiDataSourceConfig = { - type: "fmdapi", - path: `./src/config/schemas/${dataSourceName}`, - clearOldFiles: true, - clientSuffix: "Layout", - webviewerScriptName: "ExecuteDataApi", - envNames: undefined, - layouts: [], - fmMcp: { - enabled: true, - ...(baseUrl ? { baseUrl } : {}), - ...(connectedFileName ? { connectedFileName } : {}), - }, - }; - - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - let fileContent = await readJsonConfigFile(jsonConfigPath); - - if (!fileContent) { - fileContent = { - $schema: "https://proofkit.proof.sh/typegen-config-schema.json", - config: [newConfig], - }; - await writeJsonConfigFile(jsonConfigPath, fileContent); - return; - } - - const configArray = Array.isArray(fileContent.config) ? fileContent.config : [fileContent.config]; - if (!Array.isArray(fileContent.config)) { - fileContent.config = configArray; - } - - const existingConfigIndex = configArray.findIndex( - (config): config is FmdapiDataSourceConfig => config.type === "fmdapi" && config.path === newConfig.path, - ); - - if (existingConfigIndex === -1) { - configArray.push(newConfig); - } else { - const existingConfig = configArray[existingConfigIndex] as FmdapiDataSourceConfig; - configArray[existingConfigIndex] = { - ...existingConfig, - ...newConfig, - layouts: existingConfig.layouts ?? [], - fmMcp: { - enabled: true, - ...(existingConfig.fmMcp ?? {}), - ...(newConfig.fmMcp ?? {}), - }, - }; - } - - await writeJsonConfigFile(jsonConfigPath, fileContent); -} - -export async function runCodegenCommand() { - const projectDir = state.projectDir; - const config = await readJsonConfigFile(path.join(projectDir, typegenConfigFileName)); - if (!config) { - logger.info("no typegen config found, skipping typegen"); - return; - } - - // make sure to load the .env file - dotenvConfig({ path: path.join(projectDir, ".env") }); - await generateTypedClients(config.config, { cwd: projectDir }); -} - -export function getClientSuffix({ - projectDir = process.cwd(), - dataSourceName, -}: { - projectDir?: string; - dataSourceName: string; -}): string { - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - if (!fs.existsSync(jsonConfigPath)) { - return "Client"; - } - try { - const fileContent = fs.readFileSync(jsonConfigPath, "utf8"); - const parsed = parseJsonc(fileContent) as FullProofkitTypegenJsonFile; - - // Normalize config to add default type for backwards compatibility - const normalizedConfig = normalizeConfig(parsed.config); - const configToSearch = Array.isArray(normalizedConfig) ? normalizedConfig : [normalizedConfig]; - - const targetDataSource = configToSearch.find( - (ds): ds is FmdapiDataSourceConfig => - ds.type === "fmdapi" && - (ds.path?.endsWith(dataSourceName) || ds.path?.endsWith(`${dataSourceName}/`) || ds.path === dataSourceName), - ); - return targetDataSource?.clientSuffix ?? "Client"; - } catch (error) { - console.error(`Error reading or parsing JSONC config for getClientSuffix: ${jsonConfigPath}`, error); - return "Client"; - } -} - -export function getExistingSchemas({ - projectDir = process.cwd(), - dataSourceName, -}: { - projectDir?: string; - dataSourceName: string; -}): { layout?: string; schemaName?: string }[] { - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - if (!fs.existsSync(jsonConfigPath)) { - return []; - } - try { - const fileContent = fs.readFileSync(jsonConfigPath, "utf8"); - const parsed = parseJsonc(fileContent) as FullProofkitTypegenJsonFile; - - // Normalize config to add default type for backwards compatibility - const normalizedConfig = normalizeConfig(parsed.config); - const configToSearch = Array.isArray(normalizedConfig) ? normalizedConfig : [normalizedConfig]; - - const targetDataSource = configToSearch.find( - (ds): ds is FmdapiDataSourceConfig => - ds.type === "fmdapi" && - (ds.path?.endsWith(dataSourceName) || ds.path?.endsWith(`${dataSourceName}/`) || ds.path === dataSourceName), - ); - - if (targetDataSource?.layouts) { - return targetDataSource.layouts.map((layout) => ({ - layout: layout.layoutName, - schemaName: layout.schemaName, - })); - } - return []; - } catch (error) { - console.error(`Error reading or parsing JSONC config for getExistingSchemas: ${jsonConfigPath}`, error); - return []; - } -} - -export async function addToFmschemaConfig({ - dataSourceName, - envNames, -}: { - dataSourceName: string; - envNames?: z.infer; -}) { - const projectDir = state.projectDir; - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - let fileContent = await readJsonConfigFile(jsonConfigPath); - - const newDataSource: FmdapiDataSourceConfig = { - type: "fmdapi", - layouts: [], - path: `./src/config/schemas/${dataSourceName}`, - envNames: undefined, - clearOldFiles: true, - clientSuffix: "Layout", - }; - - if (envNames) { - newDataSource.envNames = { - server: envNames.server, - db: envNames.database, - auth: { apiKey: envNames.apiKey }, - }; - } - if (state.appType === "webviewer") { - newDataSource.webviewerScriptName = "ExecuteDataApi"; - } - - if (fileContent) { - let configArray: AnyDataSourceConfig[]; - if (Array.isArray(fileContent.config)) { - configArray = fileContent.config; - } else { - configArray = [fileContent.config]; - fileContent.config = configArray; - } - - const existingDsIndex = configArray.findIndex((ds) => ds.type === "fmdapi" && ds.path === newDataSource.path); - if (existingDsIndex === -1) { - configArray.push(newDataSource); - } else { - const existingConfig = configArray[existingDsIndex] as FmdapiDataSourceConfig; - configArray[existingDsIndex] = { - ...existingConfig, - ...newDataSource, - layouts: newDataSource.layouts.length > 0 ? newDataSource.layouts : existingConfig.layouts || [], - }; - } - } else { - fileContent = { - $schema: "https://proofkit.proof.sh/typegen-config-schema.json", - config: [newDataSource], - }; - } - await writeJsonConfigFile(jsonConfigPath, fileContent); -} - -export function getFieldNamesForSchema({ schemaName, dataSourceName }: { schemaName: string; dataSourceName: string }) { - const projectDir = state.projectDir; - const project = getNewProject(projectDir); - const sourceFilePath = path.join(projectDir, `src/config/schemas/${dataSourceName}/generated/${schemaName}.ts`); - - const sourceFilePathAlternative = path.join(projectDir, `src/config/schemas/${dataSourceName}/${schemaName}.ts`); - - let fileToUse = sourceFilePath; - if (!fs.existsSync(sourceFilePath)) { - if (fs.existsSync(sourceFilePathAlternative)) { - fileToUse = sourceFilePathAlternative; - } else { - return []; - } - } - const sourceFile = project.addSourceFileAtPath(fileToUse); - - const zodSchema = sourceFile.getVariableDeclaration(`Z${schemaName}`); - if (zodSchema) { - const properties = zodSchema - .getInitializer() - ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression) - ?.getProperties(); - return ( - properties?.map((pr) => pr.asKind(SyntaxKind.PropertyAssignment)?.getName()?.replace(/"/g, "")).filter(Boolean) ?? - [] - ); - } - const typeAlias = sourceFile.getTypeAlias(`T${schemaName}`); - const properties = typeAlias?.getFirstDescendantByKind(SyntaxKind.TypeLiteral)?.getProperties(); - return ( - properties?.map((pr) => pr.asKind(SyntaxKind.PropertySignature)?.getName()?.replace(/"/g, "")).filter(Boolean) ?? [] - ); -} - -export async function removeFromFmschemaConfig({ dataSourceName }: { dataSourceName: string }) { - const projectDir = state.projectDir; - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - const fileContent = await readJsonConfigFile(jsonConfigPath); - - if (!fileContent) { - return; - } - - const pathToRemove = `./src/config/schemas/${dataSourceName}`; - - if (Array.isArray(fileContent.config)) { - fileContent.config = fileContent.config.filter((ds) => !(ds.type === "fmdapi" && ds.path === pathToRemove)); - } else { - const currentConfig = fileContent.config; - if (currentConfig.type === "fmdapi" && currentConfig.path === pathToRemove) { - fileContent.config = []; - } - } - await writeJsonConfigFile(jsonConfigPath, fileContent); -} - -export async function removeLayout({ - projectDir = state.projectDir, - schemaName, - dataSourceName, - runCodegen = true, -}: { - projectDir?: string; - schemaName: string; - dataSourceName: string; - runCodegen?: boolean; -}) { - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - const fileContent = await readJsonConfigFile(jsonConfigPath); - - if (!fileContent) { - throw new Error(`${typegenConfigFileName} not found, cannot remove layout.`); - } - - let dataSourceModified = false; - const targetDsPath = `./src/config/schemas/${dataSourceName}`; - - let configArray: AnyDataSourceConfig[]; - if (Array.isArray(fileContent.config)) { - configArray = fileContent.config; - } else { - configArray = [fileContent.config]; - fileContent.config = configArray; - } - - const targetDataSource = configArray.find( - (ds): ds is FmdapiDataSourceConfig => ds.type === "fmdapi" && ds.path === targetDsPath, - ); - - if (targetDataSource?.layouts) { - const initialCount = targetDataSource.layouts.length; - targetDataSource.layouts = targetDataSource.layouts.filter((layout) => layout.schemaName !== schemaName); - if (targetDataSource.layouts.length < initialCount) { - dataSourceModified = true; - } - } - - if (dataSourceModified) { - await writeJsonConfigFile(jsonConfigPath, fileContent); - } - - const schemaFilePath = path.join(projectDir, "src", "config", "schemas", dataSourceName, `${schemaName}.ts`); - if (fs.existsSync(schemaFilePath)) { - fs.removeSync(schemaFilePath); - } - - if (runCodegen && dataSourceModified) { - await runCodegenCommand(); - } -} - -// Make sure to remove unused imports like Project, SyntaxKind, etc. if they are no longer used anywhere. -// Also remove getNewProject and formatAndSaveSourceFiles from imports if they were only for config. diff --git a/packages/cli-old/src/generators/route.ts b/packages/cli-old/src/generators/route.ts deleted file mode 100644 index e008a05c..00000000 --- a/packages/cli-old/src/generators/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; -import type { RouteLink } from "index.js"; -import { SyntaxKind } from "ts-morph"; - -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; - -export async function addRouteToNav({ - projectDir, - navType, - ...route -}: Omit & { - projectDir: string; - navType: "primary" | "secondary"; -}) { - const navFilePath = path.join(projectDir, "src/app/navigation.tsx"); - - // If the navigation file doesn't exist (e.g., Web Viewer apps), skip adding to nav - if (!fs.existsSync(navFilePath)) { - return; - } - - const project = getNewProject(projectDir); - const sourceFile = project.addSourceFileAtPath(navFilePath); - sourceFile - .getVariableDeclaration(navType === "primary" ? "primaryRoutes" : "secondaryRoutes") - ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression) - ?.addElement((writer) => - writer - .block(() => { - writer.write(` - label: "${route.label}", - type: "link", - href: "${route.href}",`); - }) - .write(","), - ); - - await formatAndSaveSourceFiles(project); -} diff --git a/packages/cli-old/src/generators/tanstack-query.ts b/packages/cli-old/src/generators/tanstack-query.ts deleted file mode 100644 index 874eee0d..00000000 --- a/packages/cli-old/src/generators/tanstack-query.ts +++ /dev/null @@ -1,97 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; -import { type Project, SyntaxKind } from "ts-morph"; - -import { PKG_ROOT } from "~/consts.js"; -import { state } from "~/state.js"; -import { addPackageDependency } from "~/utils/addPackageDependency.js"; -import { getSettings, setSettings } from "~/utils/parseSettings.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; - -export async function injectTanstackQuery(args?: { project?: Project }) { - const projectDir = state.projectDir; - const settings = getSettings(); - if (settings.ui === "shadcn") { - return false; - } - if (settings.tanstackQuery) { - return false; - } - - addPackageDependency({ - projectDir, - dependencies: ["@tanstack/react-query"], - devMode: false, - }); - addPackageDependency({ - projectDir, - dependencies: ["@tanstack/react-query-devtools"], - devMode: true, - }); - const extrasDir = path.join(PKG_ROOT, "template", "extras"); - - if (state.appType === "browser") { - fs.copySync( - path.join(extrasDir, "config", "get-query-client.ts"), - path.join(projectDir, "src/config/get-query-client.ts"), - ); - fs.copySync( - path.join(extrasDir, "config", "query-provider.tsx"), - path.join(projectDir, "src/config/query-provider.tsx"), - ); - } else if (state.appType === "webviewer") { - fs.copySync( - path.join(extrasDir, "config", "query-provider-vite.tsx"), - path.join(projectDir, "src/config/query-provider.tsx"), - ); - } - - // inject query provider into the root layout - const project = args?.project ?? getNewProject(projectDir); - const rootLayout = project.addSourceFileAtPath( - path.join(projectDir, state.appType === "browser" ? "src/app/layout.tsx" : "src/main.tsx"), - ); - rootLayout.addImportDeclaration({ - moduleSpecifier: "@/config/query-provider", - defaultImport: "QueryProvider", - }); - - if (state.appType === "browser") { - const exportDefault = rootLayout.getFunction((dec) => dec.isDefaultExport()); - const bodyElement = exportDefault - ?.getBody() - ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement) - ?.getDescendantsOfKind(SyntaxKind.JsxOpeningElement) - .find((openingElement) => openingElement.getTagNameNode().getText() === "body") - ?.getParentIfKind(SyntaxKind.JsxElement); - - const childrenText = bodyElement - ?.getJsxChildren() - .map((child) => child.getText()) - .filter(Boolean) - .join("\n"); - - bodyElement?.getChildSyntaxList()?.replaceWithText( - ` - ${childrenText} - `, - ); - } else if (state.appType === "webviewer") { - const mantineProvider = rootLayout - .getDescendantsOfKind(SyntaxKind.JsxElement) - .find((element) => element.getOpeningElement().getTagNameNode().getText() === "MantineProvider"); - - mantineProvider?.replaceWithText( - ` - ${mantineProvider.getText()} - `, - ); - } - - if (!args?.project) { - await formatAndSaveSourceFiles(project); - } - - setSettings({ ...settings, tanstackQuery: true }); - return true; -} diff --git a/packages/cli-old/src/globalOptions.ts b/packages/cli-old/src/globalOptions.ts deleted file mode 100644 index 5fbdeef7..00000000 --- a/packages/cli-old/src/globalOptions.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Option } from "commander"; - -export const ciOption = new Option("--ci", "Deprecated alias for --non-interactive").default(false); -export const nonInteractiveOption = new Option( - "--non-interactive", - "Never prompt for input; fail with a clear error when required values are missing", -).default(false); -export const debugOption = new Option("--debug", "Run in debug mode").default(false); diff --git a/packages/cli-old/src/globals.d.ts b/packages/cli-old/src/globals.d.ts deleted file mode 100644 index edd6438c..00000000 --- a/packages/cli-old/src/globals.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare const __FMDAPI_VERSION__: string; -declare const __BETTER_AUTH_VERSION__: string; -declare const __WEBVIEWER_VERSION__: string; -declare const __TYPEGEN_VERSION__: string; diff --git a/packages/cli-old/src/helpers/createProject.ts b/packages/cli-old/src/helpers/createProject.ts deleted file mode 100644 index cb354d95..00000000 --- a/packages/cli-old/src/helpers/createProject.ts +++ /dev/null @@ -1,129 +0,0 @@ -import path from "node:path"; - -import { installPackages } from "~/helpers/installPackages.js"; -import { scaffoldProject } from "~/helpers/scaffoldProject.js"; -import type { AvailableDependencies } from "~/installers/dependencyVersionMap.js"; -import type { PkgInstallerMap } from "~/installers/index.js"; -import { state } from "~/state.js"; -import { addPackageDependency } from "~/utils/addPackageDependency.js"; -import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; -import { replaceTextInFiles } from "./replaceText.js"; - -interface CreateProjectOptions { - projectName: string; - packages: PkgInstallerMap; - scopedAppName: string; - noInstall: boolean; - force: boolean; - appRouter: boolean; -} - -export const createBareProject = async ({ - projectName, - scopedAppName, - packages, - noInstall, - force, -}: CreateProjectOptions) => { - const pkgManager = getUserPkgManager(); - state.projectDir = path.resolve(process.cwd(), projectName); - - // Bootstraps the base Next.js application - await scaffoldProject({ - projectName, - pkgManager, - scopedAppName, - noInstall, - force, - }); - - addPackageDependency({ - dependencies: ["@proofkit/cli", "@types/node"], - devMode: true, - }); - - // Add new base dependencies for Tailwind v4 and shadcn/ui or legacy Mantine - // These should match the plan and dependencyVersionMap - const NEXT_SHADCN_BASE_DEPS = [ - "@radix-ui/react-slot", - "@tailwindcss/postcss", - "class-variance-authority", - "clsx", - "lucide-react", - "tailwind-merge", - "tailwindcss", - "tw-animate-css", - "next-themes", - ] as AvailableDependencies[]; - const VITE_SHADCN_BASE_DEPS = [ - "@radix-ui/react-slot", - "@tailwindcss/vite", - "@proofkit/fmdapi", - "@proofkit/webviewer", - "class-variance-authority", - "clsx", - "lucide-react", - "tailwind-merge", - "tailwindcss", - "tw-animate-css", - "zod", - ] as AvailableDependencies[]; - const SHADCN_BASE_DEV_DEPS = [] as AvailableDependencies[]; - const VITE_SHADCN_BASE_DEV_DEPS = ["@proofkit/typegen"] as AvailableDependencies[]; - - const MANTINE_DEPS = [ - "@mantine/core", - "@mantine/dates", - "@mantine/hooks", - "@mantine/modals", - "@mantine/notifications", - "mantine-react-table", - ] as AvailableDependencies[]; - const MANTINE_DEV_DEPS = ["postcss", "postcss-preset-mantine", "postcss-simple-vars"] as AvailableDependencies[]; - - if (state.ui === "mantine") { - addPackageDependency({ - dependencies: MANTINE_DEPS, - devMode: false, - }); - addPackageDependency({ - dependencies: MANTINE_DEV_DEPS, - devMode: true, - }); - } else if (state.ui === "shadcn") { - addPackageDependency({ - dependencies: state.appType === "webviewer" ? VITE_SHADCN_BASE_DEPS : NEXT_SHADCN_BASE_DEPS, - devMode: false, - }); - addPackageDependency({ - dependencies: state.appType === "webviewer" ? VITE_SHADCN_BASE_DEV_DEPS : SHADCN_BASE_DEV_DEPS, - devMode: true, - }); - } else { - throw new Error(`Unsupported UI library: ${state.ui}`); - } - - // Install the selected packages - installPackages({ - projectName, - scopedAppName, - pkgManager, - packages, - noInstall, - }); - - let pkgManagerCommand: string; - if (pkgManager === "pnpm") { - pkgManagerCommand = "pnpm"; - } else if (pkgManager === "bun") { - pkgManagerCommand = "bun"; - } else if (pkgManager === "yarn") { - pkgManagerCommand = "yarn"; - } else { - pkgManagerCommand = "npm run"; - } - - replaceTextInFiles(state.projectDir, "__PNPM_COMMAND__", pkgManagerCommand); - - return state.projectDir; -}; diff --git a/packages/cli-old/src/helpers/fmMcp.ts b/packages/cli-old/src/helpers/fmMcp.ts deleted file mode 100644 index ab58114e..00000000 --- a/packages/cli-old/src/helpers/fmMcp.ts +++ /dev/null @@ -1,56 +0,0 @@ -const defaultBaseUrl = process.env.FM_MCP_BASE_URL ?? "http://127.0.0.1:1365"; -const REQUEST_TIMEOUT_MS = 3000; - -export interface FmMcpStatus { - baseUrl: string; - healthy: boolean; - connectedFiles: string[]; -} - -async function fetchWithTimeout(url: string): Promise { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); - - try { - return await fetch(url, { signal: controller.signal }); - } catch { - return null; - } finally { - clearTimeout(timeoutId); - } -} - -async function readJson(url: string): Promise { - const response = await fetchWithTimeout(url); - - if (!response?.ok) { - return null; - } - - return await response.json().catch(() => null); -} - -export async function getFmMcpStatus(baseUrl = defaultBaseUrl): Promise { - const healthResponse = await fetchWithTimeout(`${baseUrl}/health`); - - if (!healthResponse?.ok) { - return { - baseUrl, - healthy: false, - connectedFiles: [], - }; - } - - const connectedFiles = await readJson(`${baseUrl}/connectedFiles`); - - return { - baseUrl, - healthy: true, - connectedFiles: Array.isArray(connectedFiles) ? connectedFiles : [], - }; -} - -export async function detectConnectedFmFile(baseUrl = defaultBaseUrl): Promise { - const status = await getFmMcpStatus(baseUrl); - return status.connectedFiles[0]; -} diff --git a/packages/cli-old/src/helpers/git.ts b/packages/cli-old/src/helpers/git.ts deleted file mode 100644 index bdeaefee..00000000 --- a/packages/cli-old/src/helpers/git.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { execSync } from "node:child_process"; -import path from "node:path"; -import chalk from "chalk"; -import { execa } from "execa"; -import fs from "fs-extra"; -import ora from "ora"; -import * as p from "~/cli/prompts.js"; - -import { isNonInteractiveMode } from "~/state.js"; -import { logger } from "~/utils/logger.js"; - -const isGitInstalled = (dir: string): boolean => { - try { - execSync("git --version", { cwd: dir }); - return true; - } catch (_e) { - return false; - } -}; - -/** @returns Whether or not the provided directory has a `.git` subdirectory in it. */ -export const isRootGitRepo = (dir: string): boolean => { - return fs.existsSync(path.join(dir, ".git")); -}; - -/** @returns Whether or not this directory or a parent directory has a `.git` directory. */ -export const isInsideGitRepo = async (dir: string): Promise => { - try { - // If this command succeeds, we're inside a git repo - await execa("git", ["rev-parse", "--is-inside-work-tree"], { - cwd: dir, - stdout: "ignore", - }); - return true; - } catch (_e) { - // Else, it will throw a git-error and we return false - return false; - } -}; - -const getGitVersion = () => { - const stdout = execSync("git --version").toString().trim(); - const gitVersionTag = stdout.split(" ")[2]; - const major = gitVersionTag?.split(".")[0]; - const minor = gitVersionTag?.split(".")[1]; - return { major: Number(major), minor: Number(minor) }; -}; - -/** @returns The git config value of "init.defaultBranch". If it is not set, returns "main". */ -const getDefaultBranch = () => { - const stdout = execSync("git config --global init.defaultBranch || echo main").toString().trim(); - - return stdout; -}; - -// This initializes the Git-repository for the project -export const initializeGit = async (projectDir: string) => { - logger.info("Initializing Git..."); - - if (!isGitInstalled(projectDir)) { - logger.warn("Git is not installed. Skipping Git initialization."); - return; - } - - const spinner = ora("Creating a new git repo...\n").start(); - - const isRoot = isRootGitRepo(projectDir); - const isInside = await isInsideGitRepo(projectDir); - const dirName = path.parse(projectDir).name; // skip full path for logging - - if (isInside && isRoot) { - // Dir is a root git repo - spinner.stop(); - if (isNonInteractiveMode()) { - throw new Error( - `Cannot initialize git in non-interactive mode because "${dirName}" already contains a git repository.`, - ); - } - const overwriteGit = await p.confirm({ - message: `${chalk.redBright.bold( - "Warning:", - )} Git is already initialized in "${dirName}". Initializing a new git repository would delete the previous history. Would you like to continue anyways?`, - initialValue: false, - }); - - if (!overwriteGit) { - spinner.info("Skipping Git initialization."); - return; - } - // Deleting the .git folder - fs.removeSync(path.join(projectDir, ".git")); - } else if (isInside && !isRoot) { - // Dir is inside a git worktree - spinner.stop(); - if (isNonInteractiveMode()) { - throw new Error( - `Cannot initialize git in non-interactive mode because "${dirName}" is already inside a git worktree.`, - ); - } - const initializeChildGitRepo = await p.confirm({ - message: `${chalk.redBright.bold( - "Warning:", - )} "${dirName}" is already in a git worktree. Would you still like to initialize a new git repository in this directory?`, - initialValue: false, - }); - if (!initializeChildGitRepo) { - spinner.info("Skipping Git initialization."); - return; - } - } - - // We're good to go, initializing the git repo - try { - const branchName = getDefaultBranch(); - - // --initial-branch flag was added in git v2.28.0 - const { major, minor } = getGitVersion(); - if (major < 2 || (major === 2 && minor < 28)) { - await execa("git", ["init"], { cwd: projectDir }); - // symbolic-ref is used here due to refs/heads/master not existing - // It is only created after the first commit - // https://superuser.com/a/1419674 - await execa("git", ["symbolic-ref", "HEAD", `refs/heads/${branchName}`], { - cwd: projectDir, - }); - } else { - await execa("git", ["init", `--initial-branch=${branchName}`], { - cwd: projectDir, - }); - } - await execa("git", ["add", "."], { cwd: projectDir }); - await execa("git", ["commit", "-m", "Initial commit"], { - cwd: projectDir, - }); - spinner.succeed(`${chalk.green("Successfully initialized and staged")} ${chalk.green.bold("git")}\n`); - } catch (_error) { - // Safeguard, should be unreachable - spinner.fail(`${chalk.bold.red("Failed:")} could not initialize git. Update git to the latest version!\n`); - } -}; diff --git a/packages/cli-old/src/helpers/installDependencies.ts b/packages/cli-old/src/helpers/installDependencies.ts deleted file mode 100644 index 880bd436..00000000 --- a/packages/cli-old/src/helpers/installDependencies.ts +++ /dev/null @@ -1,242 +0,0 @@ -import chalk from "chalk"; -import { execa, type StdoutStderrOption } from "execa"; -import ora, { type Ora } from "ora"; - -import { state } from "~/state.js"; -import { getUserPkgManager, type PackageManager } from "~/utils/getUserPkgManager.js"; -import { logger } from "~/utils/logger.js"; - -const execWithSpinner = async ( - projectDir: string, - pkgManager: PackageManager | "pnpx" | "bunx", - options: { - args?: string[]; - stdout?: StdoutStderrOption; - onDataHandle?: (spinner: Ora) => (data: Buffer) => void; - loadingMessage?: string; - }, -) => { - const { onDataHandle, args = ["install"], stdout = "pipe" } = options; - - if (process.env.PROOFKIT_ENV === "development") { - args.push("--prefer-offline"); - } - - const spinner = ora(options.loadingMessage ?? `Running ${pkgManager} ${args.join(" ")} ...`).start(); - const subprocess = execa(pkgManager, args, { - cwd: projectDir, - stdout, - stderr: "pipe", // Capture stderr to get error messages - }); - - await new Promise((res, rej) => { - let stdoutOutput = ""; - let stderrOutput = ""; - - if (onDataHandle) { - subprocess.stdout?.on("data", onDataHandle(spinner)); - } else { - // If no custom handler, capture stdout for error reporting - subprocess.stdout?.on("data", (data) => { - stdoutOutput += data.toString(); - }); - } - - // Capture stderr output for error reporting - subprocess.stderr?.on("data", (data) => { - stderrOutput += data.toString(); - }); - - subprocess.on("error", (e) => rej(e)); - subprocess.on("close", (code) => { - if (code === 0) { - res(); - } else { - // Combine stdout and stderr for complete error message - const combinedOutput = [stdoutOutput, stderrOutput] - .filter((output) => output.trim()) - .join("\n") - .trim() - // Remove spinner-related lines that aren't useful in error output - .replace(/^- Checking registry\.$/gm, "") - .replace(/^\s*$/gm, "") // Remove empty lines - .trim(); - - const errorMessage = combinedOutput || `Command failed with exit code ${code}: ${pkgManager} ${args.join(" ")}`; - rej(new Error(errorMessage)); - } - }); - }); - - return spinner; -}; - -const runInstallCommand = async (pkgManager: PackageManager, projectDir: string): Promise => { - switch (pkgManager) { - // When using npm, inherit the stderr stream so that the progress bar is shown - case "npm": - await execa(pkgManager, ["install"], { - cwd: projectDir, - stderr: "inherit", - }); - - return null; - // When using yarn or pnpm, use the stdout stream and ora spinner to show the progress - case "pnpm": - return execWithSpinner(projectDir, pkgManager, { - onDataHandle: (spinner) => (data) => { - const text = data.toString(); - - if (text.includes("Progress")) { - spinner.text = text.includes("|") ? (text.split(" | ")[1] ?? "") : text; - } - }, - }); - case "yarn": - return execWithSpinner(projectDir, pkgManager, { - onDataHandle: (spinner) => (data) => { - spinner.text = data.toString(); - }, - }); - // When using bun, the stdout stream is ignored and the spinner is shown - case "bun": - return execWithSpinner(projectDir, pkgManager, { stdout: "ignore" }); - default: - throw new Error(`Unknown package manager: ${pkgManager}`); - } -}; - -export const installDependencies = async (args?: { projectDir?: string }) => { - const { projectDir = state.projectDir } = args ?? {}; - logger.info("Installing dependencies..."); - const pkgManager = getUserPkgManager(); - - const installSpinner = await runInstallCommand(pkgManager, projectDir); - - // If the spinner was used to show the progress, use succeed method on it - // If not, use the succeed on a new spinner - (installSpinner ?? ora()).succeed(chalk.green("Successfully installed dependencies!\n")); -}; - -export const runExecCommand = async ({ - command, - projectDir = state.projectDir, - successMessage, - errorMessage, - loadingMessage, -}: { - command: string[]; - projectDir?: string; - successMessage?: string; - errorMessage?: string; - loadingMessage?: string; -}) => { - let spinner: Ora | null = null; - - try { - spinner = await _runExecCommand({ - projectDir, - command, - loadingMessage, - }); - - // If the spinner was used to show the progress, use succeed method on it - // If not, use the succeed on a new spinner - (spinner ?? ora()).succeed( - chalk.green(successMessage ? `${successMessage}\n` : `Successfully ran ${command.join(" ")}!\n`), - ); - } catch (error) { - // If we have a spinner, fail it, otherwise just throw the error - if (spinner) { - const failMessage = errorMessage || `Failed to run ${command.join(" ")}`; - spinner.fail(chalk.red(failMessage)); - } - throw error; - } -}; - -export const _runExecCommand = async ({ - projectDir, - command, - loadingMessage, -}: { - projectDir: string; - exec?: boolean; - command: string[]; - loadingMessage?: string; -}): Promise => { - const pkgManager = getUserPkgManager(); - switch (pkgManager) { - // When using npm, capture both stdout and stderr to show error messages - case "npm": { - const result = await execa("npx", [...command], { - cwd: projectDir, - stdout: "pipe", - stderr: "pipe", - reject: false, - }); - - if (result.exitCode !== 0) { - // Combine stdout and stderr for complete error message - const combinedOutput = [result.stdout, result.stderr] - .filter((output) => output?.trim()) - .join("\n") - .trim() - // Remove spinner-related lines that aren't useful in error output - .replace(/^- Checking registry\.$/gm, "") - .replace(/^\s*$/gm, "") // Remove empty lines - .trim(); - - const errorMessage = - combinedOutput || `Command failed with exit code ${result.exitCode}: npx ${command.join(" ")}`; - throw new Error(errorMessage); - } - - return null; - } - // When using yarn or pnpm, use the stdout stream and ora spinner to show the progress - case "pnpm": { - // For shadcn commands, don't use progress handler to capture full output - const isInstallCommand = command.includes("install"); - return execWithSpinner(projectDir, "pnpm", { - args: ["dlx", ...command], - loadingMessage, - onDataHandle: isInstallCommand - ? (spinner) => (data) => { - const text = data.toString(); - - if (text.includes("Progress")) { - spinner.text = text.includes("|") ? (text.split(" | ")[1] ?? "") : text; - } - } - : undefined, - }); - } - case "yarn": { - // For shadcn commands, don't use progress handler to capture full output - const isYarnInstallCommand = command.includes("install"); - return execWithSpinner(projectDir, pkgManager, { - args: [...command], - loadingMessage, - onDataHandle: isYarnInstallCommand - ? (spinner) => (data) => { - spinner.text = data.toString(); - } - : undefined, - }); - } - // When using bun, the stdout stream is ignored and the spinner is shown - case "bun": - return execWithSpinner(projectDir, "bunx", { - stdout: "ignore", - args: [...command], - loadingMessage, - }); - default: - throw new Error(`Unknown package manager: ${pkgManager}`); - } -}; - -export function generateRandomSecret(): string { - return crypto.randomUUID().replace(/-/g, ""); -} diff --git a/packages/cli-old/src/helpers/installPackages.ts b/packages/cli-old/src/helpers/installPackages.ts deleted file mode 100644 index 06345c47..00000000 --- a/packages/cli-old/src/helpers/installPackages.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { InstallerOptions, PkgInstallerMap } from "~/installers/index.js"; -import { logger } from "~/utils/logger.js"; - -type InstallPackagesOptions = InstallerOptions & { - packages: PkgInstallerMap; -}; -// This runs the installer for all the packages that the user has selected -export const installPackages = (options: InstallPackagesOptions) => { - const { packages } = options; - logger.info("Adding boilerplate..."); - - for (const [_name, pkgOpts] of Object.entries(packages)) { - if (pkgOpts.inUse) { - // const spinner = ora(`Boilerplating ${name}...`).start(); - pkgOpts.installer(options); - // spinner.succeed( - // chalk.green( - // `Successfully setup boilerplate for ${chalk.green.bold(name)}` - // ) - // ); - } - } - - logger.info(""); -}; diff --git a/packages/cli-old/src/helpers/logNextSteps.ts b/packages/cli-old/src/helpers/logNextSteps.ts deleted file mode 100644 index 5b7c845d..00000000 --- a/packages/cli-old/src/helpers/logNextSteps.ts +++ /dev/null @@ -1,48 +0,0 @@ -import chalk from "chalk"; - -import { DEFAULT_APP_NAME } from "~/consts.js"; -import type { InstallerOptions } from "~/installers/index.js"; -import { state } from "~/state.js"; -import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; -import { logger } from "~/utils/logger.js"; - -const formatRunCommand = (pkgManager: ReturnType, command: string) => - ["npm", "bun"].includes(pkgManager) ? `${pkgManager} run ${command}` : `${pkgManager} ${command}`; - -// This logs the next steps that the user should take in order to advance the project -export const logNextSteps = ({ - projectName = DEFAULT_APP_NAME, - noInstall, -}: Pick) => { - const pkgManager = getUserPkgManager(); - - logger.info(chalk.bold("Next steps:")); - logger.dim("\nNavigate to the project directory:"); - projectName !== "." && logger.info(` cd ${projectName}`); - logger.dim("(or open in your code editor, and run the rest of these commands from there)"); - - if (noInstall) { - logger.dim("\nInstall dependencies:"); - // To reflect yarn's default behavior of installing packages when no additional args provided - if (pkgManager === "yarn") { - logger.info(` ${pkgManager}`); - } else { - logger.info(` ${pkgManager} install`); - } - } - - logger.dim("\nStart the dev server to view your app in a browser:"); - logger.info(` ${formatRunCommand(pkgManager, "dev")}`); - - if (state.appType === "webviewer") { - logger.dim("\nWhen you're ready to generate FileMaker clients:"); - logger.info(` ${formatRunCommand(pkgManager, "typegen")}`); - - logger.dim("\nTo open the starter inside FileMaker once your file is ready:"); - logger.info(` ${formatRunCommand(pkgManager, "launch-fm")}`); - } - - logger.dim("\nOr, run the ProofKit command again to add more to your project:"); - logger.info(` ${formatRunCommand(pkgManager, "proofkit")}`); - logger.dim("(Must be inside the project directory)"); -}; diff --git a/packages/cli-old/src/helpers/replaceText.ts b/packages/cli-old/src/helpers/replaceText.ts deleted file mode 100644 index e7f9d4b1..00000000 --- a/packages/cli-old/src/helpers/replaceText.ts +++ /dev/null @@ -1,17 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; - -export function replaceTextInFiles(directoryPath: string, search: string, replacement: string): void { - const files = fs.readdirSync(directoryPath); - - for (const file of files) { - const filePath = path.join(directoryPath, file); - if (fs.statSync(filePath).isDirectory()) { - replaceTextInFiles(filePath, search, replacement); - } else { - const data = fs.readFileSync(filePath, "utf8"); - const updatedData = data.replace(new RegExp(search, "g"), replacement); - fs.writeFileSync(filePath, updatedData, "utf8"); - } - } -} diff --git a/packages/cli-old/src/helpers/scaffoldProject.ts b/packages/cli-old/src/helpers/scaffoldProject.ts deleted file mode 100644 index 7905eb0a..00000000 --- a/packages/cli-old/src/helpers/scaffoldProject.ts +++ /dev/null @@ -1,136 +0,0 @@ -import path from "node:path"; -import chalk from "chalk"; -import fs from "fs-extra"; -import ora from "ora"; -import * as p from "~/cli/prompts.js"; - -import { PKG_ROOT } from "~/consts.js"; -import type { InstallerOptions } from "~/installers/index.js"; -import { isNonInteractiveMode, state } from "~/state.js"; -import { logger } from "~/utils/logger.js"; - -const AGENT_METADATA_DIRS = new Set([".agents", ".claude", ".clawed", ".clinerules", ".cursor", ".windsurf"]); - -function getMeaningfulDirectoryEntries(projectDir: string): string[] { - return fs.readdirSync(projectDir).filter((entry) => { - if (AGENT_METADATA_DIRS.has(entry)) { - return false; - } - - if (entry === ".gitignore") { - return true; - } - - if (entry.startsWith(".")) { - return false; - } - - return true; - }); -} - -// This bootstraps the base Next.js application -export const scaffoldProject = async ({ - projectName, - pkgManager, - noInstall, - force = false, -}: InstallerOptions & { force?: boolean }) => { - const projectDir = state.projectDir; - - const srcDir = path.join( - PKG_ROOT, - state.appType === "browser" - ? `template/${state.ui === "mantine" ? "nextjs-mantine" : "nextjs-shadcn"}` - : "template/vite-wv", - ); - - if (noInstall) { - logger.info(""); - } else { - logger.info(`\nUsing: ${chalk.cyan.bold(pkgManager)}\n`); - } - - const spinner = ora(`Scaffolding in: ${projectDir}...\n`).start(); - - if (fs.existsSync(projectDir)) { - const meaningfulEntries = getMeaningfulDirectoryEntries(projectDir); - - if (meaningfulEntries.length === 0) { - if (projectName !== ".") { - spinner.info(`${chalk.cyan.bold(projectName)} exists but is empty, continuing...\n`); - } - } else if (force) { - spinner.info( - `${chalk.yellow("Force mode enabled:")} clearing ${chalk.cyan.bold(projectName)} before scaffolding...\n`, - ); - fs.emptyDirSync(projectDir); - spinner.start(); - // continue to scaffold after clearing - } else if (isNonInteractiveMode()) { - spinner.fail( - `${chalk.redBright.bold("Error:")} ${chalk.cyan.bold( - projectName, - )} already exists and isn't empty. Remove the existing files or choose a different directory.`, - ); - throw new Error( - `Cannot initialize into a non-empty directory in non-interactive mode: ${meaningfulEntries.join(", ")}`, - ); - } else { - spinner.stopAndPersist(); - const overwriteDir = await p.select({ - message: `${chalk.redBright.bold("Warning:")} ${chalk.cyan.bold( - projectName, - )} already exists and isn't empty. How would you like to proceed?`, - options: [ - { - label: "Abort installation (recommended)", - value: "abort", - }, - { - label: "Clear the directory and continue installation", - value: "clear", - }, - { - label: "Continue installation and overwrite conflicting files", - value: "overwrite", - }, - ], - initialValue: "abort", - }); - if (overwriteDir === "abort") { - spinner.fail("Aborting installation..."); - process.exit(1); - } - - const overwriteAction = overwriteDir === "clear" ? "clear the directory" : "overwrite conflicting files"; - - const confirmOverwriteDir = await p.confirm({ - message: `Are you sure you want to ${overwriteAction}?`, - initialValue: false, - }); - - if (!confirmOverwriteDir) { - spinner.fail("Aborting installation..."); - process.exit(1); - } - - if (overwriteDir === "clear") { - spinner.info(`Emptying ${chalk.cyan.bold(projectName)} and creating new ProofKit app..\n`); - fs.emptyDirSync(projectDir); - } - } - } - - spinner.start(); - - // Copy the main template - fs.copySync(srcDir, projectDir); - - // Rename gitignore - fs.renameSync(path.join(projectDir, "_gitignore"), path.join(projectDir, ".gitignore")); - - const scaffoldedName = projectName === "." ? "App" : chalk.cyan.bold(projectName); - - spinner.succeed(`${scaffoldedName} ${chalk.green("scaffolded successfully!")}\n`); -}; diff --git a/packages/cli-old/src/helpers/selectBoilerplate.ts b/packages/cli-old/src/helpers/selectBoilerplate.ts deleted file mode 100644 index 4b538d3d..00000000 --- a/packages/cli-old/src/helpers/selectBoilerplate.ts +++ /dev/null @@ -1,32 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; - -import { PKG_ROOT } from "~/consts.js"; -import type { InstallerOptions } from "~/installers/index.js"; -import { state } from "~/state.js"; - -type SelectBoilerplateProps = Required>; - -export const selectLayoutFile = (_props: SelectBoilerplateProps) => { - const projectDir = state.projectDir; - const layoutFileDir = path.join(PKG_ROOT, "template/extras/src/app/layout"); - - const layoutFile = "base.tsx"; - - const appSrc = path.join(layoutFileDir, layoutFile); // base layout - const appDest = path.join(projectDir, "src/app/layout.tsx"); - fs.copySync(appSrc, appDest); - - fs.copySync(path.join(layoutFileDir, "main-shell.tsx"), path.join(projectDir, "src/app/(main)/layout.tsx")); -}; - -export const selectPageFile = (_props: SelectBoilerplateProps) => { - const projectDir = state.projectDir; - const indexFileDir = path.join(PKG_ROOT, "template/extras/src/app/page"); - - const indexFile = "base.tsx"; - - const indexSrc = path.join(indexFileDir, indexFile); - const indexDest = path.join(projectDir, "src/app/(main)/page.tsx"); - fs.copySync(indexSrc, indexDest); -}; diff --git a/packages/cli-old/src/helpers/setImportAlias.ts b/packages/cli-old/src/helpers/setImportAlias.ts deleted file mode 100644 index 7551134b..00000000 --- a/packages/cli-old/src/helpers/setImportAlias.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { replaceTextInFiles } from "./replaceText.js"; - -const TRAILING_SLASH_REGEX = /[^/]$/; - -export const setImportAlias = (projectDir: string, importAlias: string) => { - const normalizedImportAlias = importAlias - .replace(/\*/g, "") // remove any wildcards (~/* -> ~/) - .replace(TRAILING_SLASH_REGEX, "$&/"); // ensure trailing slash (@ -> ~/) - - // update import alias in any files if not using the default - replaceTextInFiles(projectDir, "~/", normalizedImportAlias); -}; diff --git a/packages/cli-old/src/helpers/shadcn-cli.ts b/packages/cli-old/src/helpers/shadcn-cli.ts deleted file mode 100644 index 4c235380..00000000 --- a/packages/cli-old/src/helpers/shadcn-cli.ts +++ /dev/null @@ -1,80 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { execa } from "execa"; - -import { DEFAULT_REGISTRY_URL } from "~/consts.js"; -import { state } from "~/state.js"; -import { logger } from "~/utils/logger.js"; -import { getSettings } from "~/utils/parseSettings.js"; - -export async function shadcnInstall(components: string | string[], _friendlyComponentName?: string) { - const componentsArray = Array.isArray(components) ? components : [components]; - const command = ["shadcn@latest", "add", ...componentsArray]; - // Use execa to run the shadcn add command directly - - try { - await execa("pnpm", ["dlx", ...command], { - stdio: "inherit", - cwd: state.projectDir ?? process.cwd(), - }); - } catch (error) { - logger.error(`Failed to run shadcn add: ${error}`); - throw error; - } -} - -export function getRegistryUrl(): string { - let url: string; - try { - url = getSettings().registryUrl ?? DEFAULT_REGISTRY_URL; - } catch { - // If we can't get settings (e.g., during development or outside a ProofKit project), - // fall back to the default registry URL - url = DEFAULT_REGISTRY_URL; - } - return url.endsWith("/") ? url.slice(0, -1) : url; -} - -export interface ShadcnConfig { - style: "default" | "new-york"; - tailwind: { - config: string; - css: string; - baseColor: string; - cssVariables: boolean; - prefix?: string; - [k: string]: unknown; - }; - rsc: boolean; - tsx?: boolean; - iconLibrary?: string; - aliases: { - utils: string; - components: string; - ui?: string; - lib?: string; - hooks?: string; - [k: string]: unknown; - }; - registries?: { - [k: string]: - | string - | { - url: string; - params?: { - [k: string]: string; - }; - headers?: { - [k: string]: string; - }; - [k: string]: unknown; - }; - }; - [k: string]: unknown; -} - -export function getShadcnConfig() { - const componentsJsonPath = path.join(state.projectDir, "components.json"); - const componentsJson = JSON.parse(fs.readFileSync(componentsJsonPath, "utf8")); - return componentsJson as ShadcnConfig; -} diff --git a/packages/cli-old/src/helpers/stealth-init.ts b/packages/cli-old/src/helpers/stealth-init.ts deleted file mode 100644 index 6f865ab8..00000000 --- a/packages/cli-old/src/helpers/stealth-init.ts +++ /dev/null @@ -1,20 +0,0 @@ -import fs from "fs-extra"; - -import { defaultSettings, setSettings, validateAndSetEnvFile } from "~/utils/parseSettings.js"; - -/** - * Used to add a proofkit.json file to an existing project - */ -export async function stealthInit() { - // check if proofkit.json exists - const proofkitJson = await fs.pathExists("proofkit.json"); - if (proofkitJson) { - return; - } - - // create proofkit.json with default settings - setSettings(defaultSettings); - - // validate and set envFile only if it exists - validateAndSetEnvFile(); -} diff --git a/packages/cli-old/src/helpers/version-fetcher.ts b/packages/cli-old/src/helpers/version-fetcher.ts deleted file mode 100644 index 26a21e80..00000000 --- a/packages/cli-old/src/helpers/version-fetcher.ts +++ /dev/null @@ -1,131 +0,0 @@ -import https from "node:https"; -import { TRPCError } from "@trpc/server"; -import axios from "axios"; -import z from "zod/v4"; - -export async function fetchServerVersions({ url, ottoPort = 3030 }: { url: string; ottoPort?: number }) { - const fmsInfo = await fetchFMSVersionInfo(url); - const ottoInfo = await fetchOttoVersion({ url, ottoPort }); - return { fmsInfo, ottoInfo }; -} - -const fmsInfoSchema = z.object({ - data: z.object({ - APIVersion: z.number().optional(), - AcceptEARPassword: z.boolean().optional(), - AcceptEncrypted: z.boolean().optional(), - AcceptUnencrypted: z.boolean().optional(), - AdminLocalAuth: z.string().optional(), - AllowChangeUploadDBFolder: z.boolean().optional(), - AutoOpenForUpload: z.boolean().optional(), - DenyGuestAndAutoLogin: z.string().optional(), - Hostname: z.string().optional(), - IsAppleInternal: z.boolean().optional(), - IsETS: z.boolean().optional(), - PremisesType: z.string().optional(), - ProductVersion: z.string().optional(), - PublicKey: z.string().optional(), - RequiresDBPasswords: z.boolean().optional(), - ServerID: z.string().optional(), - ServerVersion: z.string(), - }), - result: z.number(), -}); - -export async function fetchFMSVersionInfo(url: string) { - const fmsUrl = new URL(url); - fmsUrl.pathname = "/fmws/serverinfo"; - - const fmsInfoResult = await fetchWithoutSSL(fmsUrl.toString()).then((r) => fmsInfoSchema.safeParse(r.data)); - if (!fmsInfoResult.success) { - console.error("fmsInfoResult.error", fmsInfoResult.error.issues); - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Invalid FileMaker Server URL", - }); - } - return fmsInfoResult.data.data; -} - -const ottoInfoSchema = z.object({ - Otto: z.object({ - version: z.string(), - serverNickname: z.string().default(""), - isLicenseValid: z.boolean().optional(), - }), - migratorVersion: z.string().optional(), - FileMakerServer: z.object({ - version: z.object({ - long: z.string(), - short: z.string(), - }), - running: z.boolean().optional(), - }), - isMac: z.boolean().optional(), - platform: z.string().optional(), - host: z.string().optional(), -}); - -const ottoInfoResponseSchema = z.object({ - response: ottoInfoSchema, -}); - -export async function fetchOttoVersion({ - url, - ottoPort = 3030, -}: { - url: string; - ottoPort?: number | null; -}): Promise | null> { - let ottoInfo = await fetchOtto4Version(url); - if (!ottoInfo) { - ottoInfo = await fetchOtto3Version(url, ottoPort); - } - return ottoInfo; -} - -async function fetchOtto4Version(url: string) { - try { - const otto4Url = new URL(url); - otto4Url.pathname = "/otto/api/info"; - const otto4Info = await fetchWithoutSSL(otto4Url.toString()).then((r) => { - return ottoInfoResponseSchema.parse(r.data).response; - }); - return otto4Info; - } catch (_error) { - console.log("unable to fetch otto4 info, trying otto3"); - return null; - } -} - -async function fetchOtto3Version(url: string, ottoPort: number | null) { - try { - const otto3Url = new URL(url); - otto3Url.port = ottoPort ? ottoPort.toString() : "3030"; - otto3Url.pathname = "/api/otto/info"; - const ottoInfo = await fetchWithoutSSL(otto3Url.toString()).then((res) => { - return ottoInfoSchema.parse(res.data); - }); - return ottoInfo; - } catch (error) { - if (error instanceof Error) { - console.error("otto3 fetch error", error.message); - } - return null; - } -} - -async function fetchWithoutSSL(url: string) { - const agent = new https.Agent({ - rejectUnauthorized: false, - }); - - const result = await axios.get(url, { - validateStatus: null, - headers: { Connection: "close" }, - httpsAgent: agent, - timeout: 10_000, - }); - - return result; -} diff --git a/packages/cli-old/src/index.ts b/packages/cli-old/src/index.ts deleted file mode 100644 index a61c41c7..00000000 --- a/packages/cli-old/src/index.ts +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env node --no-warnings -import chalk from "chalk"; -import { Command } from "commander"; -import { makeInitCommand, runInit } from "~/cli/init.js"; -import { intro } from "~/cli/prompts.js"; -import { logger } from "~/utils/logger.js"; -import { proofGradient, renderTitle } from "~/utils/renderTitle.js"; -import { makeAddCommand } from "./cli/add/index.js"; -import { makeDeployCommand } from "./cli/deploy/index.js"; -import { runMenu } from "./cli/menu.js"; -import { makeRemoveCommand } from "./cli/remove/index.js"; -import { makeTypegenCommand } from "./cli/typegen/index.js"; -import { makeUpgradeCommand } from "./cli/update/makeUpgradeCommand.js"; -import { UserAbortedError } from "./cli/utils.js"; -import { npmName } from "./consts.js"; -import { ciOption, nonInteractiveOption } from "./globalOptions.js"; -import { initProgramState, isNonInteractiveMode } from "./state.js"; -import { getVersion } from "./utils/getProofKitVersion.js"; -import { getSettings, type Settings } from "./utils/parseSettings.js"; -import { checkAndRenderVersionWarning } from "./utils/renderVersionWarning.js"; - -const version = getVersion(); - -function getErrorMessage(error: unknown): string { - if (error instanceof Error) { - return error.message; - } - return String(error); -} - -const main = async () => { - const program = new Command(); - renderTitle(); - if (process.env.PROOFKIT_SKIP_VERSION_CHECK !== "1") { - await checkAndRenderVersionWarning(); - } - - program - .name(npmName) - .version(version) - .command("default", { hidden: true, isDefault: true }) - .addOption(ciOption) - .addOption(nonInteractiveOption) - .action(async (args) => { - initProgramState(args); - - let settings: Settings | undefined; - try { - settings = getSettings(); - } catch { - // void - } - - if (isNonInteractiveMode()) { - throw new Error( - "The default command is interactive-only in non-interactive mode. Run an explicit command such as `proofkit init --non-interactive`.", - ); - } - - if (settings) { - intro(`Found ${proofGradient("ProofKit")} project`); - await runMenu(); - } else { - intro(`No ${proofGradient("ProofKit")} project found, running \`init\``); - await runInit(); - } - }) - .addHelpText("afterAll", `\n The ProofKit CLI was inspired by the ${chalk.hex("#E8DCFF").bold("t3 stack")}\n`); - - program.addCommand(makeInitCommand()); - program.addCommand(makeAddCommand()); - program.addCommand(makeRemoveCommand()); - program.addCommand(makeTypegenCommand()); - program.addCommand(makeDeployCommand()); - program.addCommand(makeUpgradeCommand()); - - await program.parseAsync(process.argv); - process.exit(0); -}; - -main().catch((err) => { - if (err instanceof UserAbortedError) { - process.exit(0); - } else if (err instanceof Error) { - logger.error("Aborting installation..."); - logger.error(err.message); - const cause = (err as Error & { cause?: unknown }).cause; - if (cause) { - logger.dim(`Cause: ${getErrorMessage(cause)}`); - } - } else { - logger.error("An unknown error has occurred. Please open an issue on github with the below:"); - console.log(err); - } - process.exit(1); -}); diff --git a/packages/cli-old/src/installers/auth-shared.ts b/packages/cli-old/src/installers/auth-shared.ts deleted file mode 100644 index 20f1401d..00000000 --- a/packages/cli-old/src/installers/auth-shared.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { type SourceFile, SyntaxKind } from "ts-morph"; - -import { ensureReturnStatementIsWrappedInFragment } from "~/utils/ts-morph.js"; - -export function addToHeaderSlot(slotSourceFile: SourceFile, importFrom: string) { - slotSourceFile.addImportDeclaration({ - defaultImport: "UserMenu", - moduleSpecifier: importFrom, - }); - - // ensure Group from @mantine/core is imported - const mantineCoreImport = slotSourceFile.getImportDeclaration( - (dec) => dec.getModuleSpecifierValue() === "@mantine/core", - ); - if (mantineCoreImport) { - const groupImport = mantineCoreImport.getNamedImports().find((imp) => imp.getName() === "Group"); - - if (!groupImport) { - mantineCoreImport.addNamedImport({ name: "Group" }); - } - } else { - slotSourceFile.addImportDeclaration({ - namedImports: [{ name: "Group" }], - moduleSpecifier: "@mantine/core", - }); - } - - const returnStatement = ensureReturnStatementIsWrappedInFragment( - slotSourceFile - .getFunction((dec) => dec.isDefaultExport()) - ?.getBody() - ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement), - ); - - const existingElements = returnStatement - ?.getFirstDescendantByKind(SyntaxKind.JsxOpeningFragment) - ?.getParentIfKind(SyntaxKind.JsxFragment) - ?.getFirstDescendantByKind(SyntaxKind.SyntaxList) - ?.getText(); - - if (!existingElements) { - console.log(`Failed to inject into header slot at ${slotSourceFile.getFilePath()}`); - return; - } - - returnStatement?.replaceWithText(`return (<>${existingElements})`); - returnStatement?.formatText(); - slotSourceFile.saveSync(); -} diff --git a/packages/cli-old/src/installers/better-auth.ts b/packages/cli-old/src/installers/better-auth.ts deleted file mode 100644 index f417ab36..00000000 --- a/packages/cli-old/src/installers/better-auth.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function betterAuthInstaller() { - // TODO: Implement better-auth installer -} diff --git a/packages/cli-old/src/installers/clerk.ts b/packages/cli-old/src/installers/clerk.ts deleted file mode 100644 index 11ddd816..00000000 --- a/packages/cli-old/src/installers/clerk.ts +++ /dev/null @@ -1,153 +0,0 @@ -import path from "node:path"; -import chalk from "chalk"; -import fs from "fs-extra"; -import { type SourceFile, SyntaxKind } from "ts-morph"; - -import { PKG_ROOT } from "~/consts.js"; -import { addPackageDependency } from "~/utils/addPackageDependency.js"; -import { addToEnv } from "~/utils/addToEnvs.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; -import { addToHeaderSlot } from "./auth-shared.js"; - -export const clerkInstaller = async ({ projectDir }: { projectDir: string }) => { - addPackageDependency({ - projectDir, - dependencies: ["@clerk/nextjs", "@clerk/themes"], - devMode: false, - }); - - // add clerk middleware - // check if middleware already exists, if not add it - const extrasDir = path.join(PKG_ROOT, "template/extras"); - - const middlewareDest = path.join(projectDir, "src/middleware.ts"); - if (fs.existsSync(middlewareDest)) { - // throw new Error("Middleware already exists"); - console.log( - chalk.yellow( - "Middleware already exists. To require auth for your app, be sure to follow the guide to setup Clerk middleware. https://clerk.com/docs/references/nextjs/clerk-middleware#clerk-middleware-next-js", - ), - ); - } else { - const middlewareSrc = path.join(extrasDir, "src/middleware/clerk.ts"); - fs.copySync(middlewareSrc, middlewareDest); - } - - // copy auth pages - fs.copySync(path.join(extrasDir, "src/app/clerk-auth"), path.join(projectDir, "src/app/auth")); - - // copy auth components - fs.copySync(path.join(extrasDir, "src/components/clerk-auth"), path.join(projectDir, "src/components/clerk-auth")); - - // add ClerkProvider to app layout - const layoutFile = path.join(projectDir, "src/app/layout.tsx"); - const project = getNewProject(projectDir); - addClerkProvider(project.addSourceFileAtPath(layoutFile)); - - // inject signin/signout components to header slots - addToHeaderSlot( - project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-right.tsx")), - "@/components/clerk-auth/user-menu", - ); - addToHeaderSlot( - project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-mobile-content.tsx")), - "@/components/clerk-auth/user-menu-mobile", - ); - - addToSafeActionClient(project.addSourceFileAtPathIfExists(path.join(projectDir, "src/server/safe-action.ts"))); - - // add envs to .env and .env.schema - await addToEnv({ - projectDir, - project, - envs: [ - { - name: "NEXT_PUBLIC_CLERK_SIGN_IN_URL", - zodValue: "z.string()", - defaultValue: "/auth/signin", - type: "client", - }, - { - name: "NEXT_PUBLIC_CLERK_SIGN_UP_URL", - zodValue: "z.string()", - defaultValue: "/auth/signup", - type: "client", - }, - { - name: "CLERK_SECRET_KEY", - zodValue: `z.string().startsWith('sk_').min(1, { - message: - "No Clerk Secret Key found. Did you create your Clerk app and copy the environment variables to you .env file?", - })`, - type: "server", - }, - { - name: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", - zodValue: `z.string().startsWith('pk_').min(1, { - message: - "No Clerk Public Key found. Did you create your Clerk app and copy the environment variables to you .env file?", - })`, - type: "client", - }, - ], - envFileDescription: - "Hosted auth with Clerk. Set up a new app at https://dashboard.clerk.com/apps/new to get these values.", - }); - - await formatAndSaveSourceFiles(project); -}; - -export function addClerkProvider(sourceFile: SourceFile) { - sourceFile.addImportDeclaration({ - namedImports: [{ name: "ClerkAuthProvider" }], - moduleSpecifier: "@/components/clerk-auth/clerk-provider", - }); - - // Step 2: Wrap default exported function's return statement with ClerkProvider - const exportDefault = sourceFile.getFunction((dec) => dec.isDefaultExport()); - - // find the mantine provider in this export - const mantineProvider = exportDefault - ?.getBody() - ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement) - ?.getDescendantsOfKind(SyntaxKind.JsxOpeningElement) - .find((openingElement) => openingElement.getTagNameNode().getText() === "MantineProvider") - ?.getParentIfKind(SyntaxKind.JsxElement); - - const childrenText = mantineProvider - ?.getJsxChildren() - .map((child) => child.getText()) - .filter(Boolean) - .join("\n"); - - mantineProvider?.getChildSyntaxList()?.replaceWithText( - ` - ${childrenText} - `, - ); -} - -function addToSafeActionClient(sourceFile?: SourceFile) { - if (!sourceFile) { - console.log(chalk.yellow("Failed to inject into safe-action-client. Did you move the safe-action.ts file?")); - return; - } - - sourceFile.addImportDeclaration({ - namedImports: [{ name: "auth", alias: "getAuth" }], - moduleSpecifier: "@clerk/nextjs/server", - }); - - // add to end of file - sourceFile.addStatements((writer) => - writer.writeLine(`export const authedActionClient = actionClient.use(async ({ next, ctx }) => { - const auth = getAuth(); - if (!auth.userId) { - throw new Error("Unauthorized"); - } - return next({ ctx: { ...ctx, auth } }); -}); - -`), - ); -} diff --git a/packages/cli-old/src/installers/dependencyVersionMap.ts b/packages/cli-old/src/installers/dependencyVersionMap.ts deleted file mode 100644 index c5b53268..00000000 --- a/packages/cli-old/src/installers/dependencyVersionMap.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { getNodeMajorVersion } from "~/utils/getProofKitVersion.js"; -import { getProofkitReleaseTag } from "~/utils/proofkitReleaseChannel.js"; - -const proofkitReleaseTag = getProofkitReleaseTag(); - -/* - * This maps the necessary packages to a version. - * This improves performance significantly over fetching it from the npm registry. - */ -export const dependencyVersionMap = { - // Resolve to "latest" or "beta" based on current changeset state / versions. - "@proofkit/fmdapi": proofkitReleaseTag, - "@proofkit/webviewer": proofkitReleaseTag, - "@proofkit/cli": proofkitReleaseTag, - "@proofkit/typegen": proofkitReleaseTag, - "@proofkit/better-auth": proofkitReleaseTag, - - // NextAuth.js - "next-auth": "beta", - "next-auth-adapter-filemaker": "beta", - - "@auth/prisma-adapter": "^1.6.0", - "@auth/drizzle-adapter": "^1.1.0", - - // Prisma - prisma: "^5.14.0", - "@prisma/client": "^5.14.0", - "@prisma/adapter-planetscale": "^5.14.0", - - // Drizzle - "drizzle-orm": "^0.30.10", - "drizzle-kit": "^0.21.4", - mysql2: "^3.9.7", - "@planetscale/database": "^1.18.0", - postgres: "^3.4.4", - "@libsql/client": "^0.6.0", - - // TailwindCSS - tailwindcss: "^4.1.10", - postcss: "^8.4.41", - "@tailwindcss/postcss": "^4.1.10", - "@tailwindcss/vite": "^4.2.1", - "class-variance-authority": "^0.7.1", - clsx: "^2.1.1", - "tailwind-merge": "^3.5.0", - "tw-animate-css": "^1.4.0", - - // tRPC - "@trpc/client": "^11.0.0-rc.446", - "@trpc/server": "^11.0.0-rc.446", - "@trpc/react-query": "^11.0.0-rc.446", - "@trpc/next": "^11.0.0-rc.446", - superjson: "^2.2.1", - "server-only": "^0.0.1", - - // Clerk - "@clerk/nextjs": "^6.3.1", - "@clerk/themes": "^2.1.33", - - // Tanstack Query - "@tanstack/react-query": "^5.59.0", - "@tanstack/react-query-devtools": "^5.59.0", - - // ProofKit Auth - "@node-rs/argon2": "^2.0.2", - "@oslojs/binary": "^1.0.0", - "@oslojs/crypto": "^1.0.1", - "@oslojs/encoding": "^1.1.0", - "js-cookie": "^3.0.5", - "@types/js-cookie": "^3.0.6", - - // React Email - "@react-email/components": "^0.5.0", - "@react-email/render": "1.2.0", - "@react-email/preview-server": "^4.2.8", - "@plunk/node": "^3.0.3", - "react-email": "^4.2.8", - resend: "^4.0.0", - "@sendgrid/mail": "^8.1.4", - - // Node - "@types/node": `^${getNodeMajorVersion()}`, - - // Radix (for shadcn/ui) - "@radix-ui/react-slot": "^1.2.3", - - // Icons (for shadcn/ui) - "lucide-react": "^0.577.0", - - // better-auth - "better-auth": "^1.3.4", - "@daveyplate/better-auth-ui": "^2.1.3", - - // Mantine UI - "@mantine/core": "^7.15.0", - "@mantine/dates": "^7.15.0", - "@mantine/hooks": "^7.15.0", - "@mantine/modals": "^7.15.0", - "@mantine/notifications": "^7.15.0", - "mantine-react-table": "^2.0.0", - - // Theme utilities - "next-themes": "^0.4.6", - - // Zod - zod: "^4", -} as const; -export type AvailableDependencies = keyof typeof dependencyVersionMap; diff --git a/packages/cli-old/src/installers/envVars.ts b/packages/cli-old/src/installers/envVars.ts deleted file mode 100644 index 2eb95da9..00000000 --- a/packages/cli-old/src/installers/envVars.ts +++ /dev/null @@ -1,43 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; - -import type { Installer } from "~/installers/index.js"; -import { state } from "~/state.js"; -import { logger } from "~/utils/logger.js"; - -export type FMAuthKeys = { username: string; password: string } | { ottoApiKey: string }; - -export const initEnvFile: Installer = () => { - const envFilePath = findT3EnvFile(false) ?? "./src/config/env.ts"; - - const envContent = ` -# When adding additional environment variables, the schema in "${envFilePath}" -# should be updated accordingly. - -` - .trim() - .concat("\n"); - - const envDest = path.join(state.projectDir, ".env"); - - fs.writeFileSync(envDest, envContent, "utf-8"); -}; -export function findT3EnvFile(throwIfNotFound: false): string | null; -export function findT3EnvFile(throwIfNotFound?: true): string; -export function findT3EnvFile(throwIfNotFound?: boolean): string | null { - const possiblePaths = ["src/config/env.ts", "src/lib/env.ts", "src/env.ts", "lib/env.ts", "env.ts", "config/env.ts"]; - - for (const testPath of possiblePaths) { - const fullPath = path.join(state.projectDir, testPath); - if (fs.existsSync(fullPath)) { - return fullPath; - } - } - - if (throwIfNotFound === false) { - return null; - } - - logger.warn(`Could not find T3 env files. Run "proofkit add utils/t3-env" to initialize them.`); - throw new Error("T3 env file not found"); -} diff --git a/packages/cli-old/src/installers/index.ts b/packages/cli-old/src/installers/index.ts deleted file mode 100644 index b4fb6fb6..00000000 --- a/packages/cli-old/src/installers/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { initEnvFile } from "~/installers/envVars.js"; -import type { PackageManager } from "~/utils/getUserPkgManager.js"; - -// Turning this into a const allows the list to be iterated over for programmatically creating prompt options -// Should increase extensibility in the future -export const availablePackages = ["nextAuth", "trpc", "envVariables", "fmdapi", "webViewerFetch", "clerk"] as const; -export type AvailablePackages = (typeof availablePackages)[number]; - -export interface InstallerOptions { - pkgManager: PackageManager; - noInstall: boolean; - packages?: PkgInstallerMap; - projectName: string; - scopedAppName: string; -} - -export type Installer = (opts: InstallerOptions) => void; - -export type PkgInstallerMap = { - [pkg in AvailablePackages]?: { - inUse: boolean; - installer: Installer; - }; -}; - -export const buildPkgInstallerMap = (): PkgInstallerMap => ({ - envVariables: { - inUse: true, - installer: initEnvFile, - }, -}); diff --git a/packages/cli-old/src/installers/install-fm-addon.ts b/packages/cli-old/src/installers/install-fm-addon.ts deleted file mode 100644 index b799a68e..00000000 --- a/packages/cli-old/src/installers/install-fm-addon.ts +++ /dev/null @@ -1,53 +0,0 @@ -import os from "node:os"; -import path from "node:path"; -import chalk from "chalk"; -import fs from "fs-extra"; - -import { PKG_ROOT } from "~/consts.js"; -import { logger } from "~/utils/logger.js"; - -export async function installFmAddon({ addonName }: { addonName: "auth" | "wv" }) { - const addonDisplayName = addonName === "auth" ? "FM Auth Add-on" : "ProofKit Web Viewer"; - - let targetDir: string | null = null; - if (process.platform === "win32") { - targetDir = path.join(os.homedir(), "AppData", "Local", "FileMaker", "Extensions", "AddonModules"); - } else if (process.platform === "darwin") { - targetDir = path.join(os.homedir(), "Library", "Application Support", "FileMaker", "Extensions", "AddonModules"); - } - - if (!targetDir) { - logger.warn(`Could not install the ${addonDisplayName} addon. You will need to do this manually.`); - return; - } - - const addonDir = addonName === "auth" ? "ProofKitAuth" : "ProofKitWV"; - - await fs.copy(path.join(PKG_ROOT, `template/fm-addon/${addonDir}`), path.join(targetDir, addonDir), { - overwrite: true, - }); - - console.log(""); - console.log(chalk.bgYellow(" ACTION REQUIRED: ")); - if (addonName === "auth") { - console.log( - `${chalk.yellowBright( - "You must install the FM Auth addon in your FileMaker file to continue.", - )} ${chalk.dim("(Learn more: https://proofkit.proof.sh/auth/fm-addon)")}`, - ); - } else { - console.log( - `${chalk.yellowBright( - "You must install the ProofKit Web Viewer addon in your FileMaker file to continue.", - )} ${chalk.dim("(Learn more: https://proofkit.proof.sh/webviewer)")}`, - ); - } - const steps = [ - "Restart FileMaker Pro (if it's currently running)", - `Open your FileMaker file, go to layout mode, and install the ${addonDisplayName} addon to the file`, - "Come back here to continue the installation", - ]; - steps.forEach((step, index) => { - console.log(`${index + 1}. ${step}`); - }); -} diff --git a/packages/cli-old/src/installers/nextAuth.ts b/packages/cli-old/src/installers/nextAuth.ts deleted file mode 100644 index 163df0f9..00000000 --- a/packages/cli-old/src/installers/nextAuth.ts +++ /dev/null @@ -1,189 +0,0 @@ -import path from "node:path"; -import chalk from "chalk"; -import fs from "fs-extra"; -import ora from "ora"; -import { type SourceFile, SyntaxKind } from "ts-morph"; - -import { PKG_ROOT } from "~/consts.js"; -import { getExistingSchemas } from "~/generators/fmdapi.js"; -import { _runExecCommand, generateRandomSecret } from "~/helpers/installDependencies.js"; -import { addPackageDependency } from "~/utils/addPackageDependency.js"; -import { addToEnv } from "~/utils/addToEnvs.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; -import { addToHeaderSlot } from "./auth-shared.js"; -import { dependencyVersionMap } from "./dependencyVersionMap.js"; - -export const nextAuthInstaller = async ({ projectDir }: { projectDir: string }) => { - addPackageDependency({ - projectDir, - dependencies: ["next-auth", "next-auth-adapter-filemaker"], - devMode: false, - }); - - const extrasDir = path.join(PKG_ROOT, "template/extras"); - - const routeHandlerFile = "src/app/api/auth/[...nextauth]/route.ts"; - const srcToUse = routeHandlerFile; - - const apiHandlerSrc = path.join(extrasDir, srcToUse); - const apiHandlerDest = path.join(projectDir, srcToUse); - fs.copySync(apiHandlerSrc, apiHandlerDest); - - const authConfigSrc = path.join(extrasDir, "src/server", "next-auth", "base.ts"); - const authConfigDest = path.join(projectDir, "src/server/auth.ts"); - fs.copySync(authConfigSrc, authConfigDest); - - const passwordSrc = path.join(extrasDir, "src/server", "next-auth", "password.ts"); - const passwordDest = path.join(projectDir, "src/server/password.ts"); - fs.copySync(passwordSrc, passwordDest); - - // copy users.ts to data directory - fs.copySync(path.join(extrasDir, "src/server/data/users.ts"), path.join(projectDir, "src/server/data/users.ts")); - - // copy auth pages - fs.copySync(path.join(extrasDir, "src/app/next-auth"), path.join(projectDir, "src/app/auth")); - - // copy auth components - fs.copySync(path.join(extrasDir, "src/components/next-auth"), path.join(projectDir, "src/components/next-auth")); - - const project = getNewProject(projectDir); - - // modify root layout to wrap with session provider - addNextAuthProviderToRootLayout(project.addSourceFileAtPath(path.join(projectDir, "src/app/layout.tsx"))); - - // inject signin/signout components to header slots - addToHeaderSlot( - project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-right.tsx")), - "@/components/next-auth/user-menu", - ); - addToHeaderSlot( - project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-mobile-content.tsx")), - "@/components/next-auth/user-menu-mobile", - ); - - // add a protected safe-action-client - addToSafeActionClient(project.addSourceFileAtPathIfExists(path.join(projectDir, "src/server/safe-action.ts"))); - - // // TODO do this part in-house, maybe with execa directly - // await runExecCommand({ - // command: ["auth", "secret"], - // projectDir, - // }); - - // add middleware - fs.copySync(path.join(extrasDir, "src/middleware/next-auth.ts"), path.join(projectDir, "src/middleware.ts")); - - // add envs to .env and .env.schema - await addToEnv({ - projectDir, - project, - envs: [ - { - name: "AUTH_SECRET", - zodValue: "z.string().min(1)", - defaultValue: generateRandomSecret(), - type: "server", - }, - ], - }); - - await checkForNextAuthLayouts(projectDir); - - await formatAndSaveSourceFiles(project); -}; - -function addNextAuthProviderToRootLayout(rootLayoutSource: SourceFile) { - // Add imports - rootLayoutSource.addImportDeclaration({ - namedImports: [{ name: "NextAuthProvider" }], - moduleSpecifier: "@/components/next-auth/next-auth-provider", - }); - rootLayoutSource.addImportDeclaration({ - namedImports: [{ name: "auth" }], - moduleSpecifier: "@/server/auth", - }); - - const exportDefault = rootLayoutSource.getFunction((dec) => dec.isDefaultExport()); - - // make the function async - exportDefault?.setIsAsync(true); - - // get the session server-side - exportDefault?.getFirstDescendantByKind(SyntaxKind.Block)?.insertStatements(0, "const session = await auth();"); - - // get the body element from the return statement - const bodyElement = exportDefault - ?.getBody() - ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement) - ?.getDescendantsOfKind(SyntaxKind.JsxOpeningElement) - .find((openingElement) => openingElement.getTagNameNode().getText() === "body") - ?.getParentIfKind(SyntaxKind.JsxElement); - - // wrap the body element with the next auth provider - bodyElement?.replaceWithText( - ` - ${bodyElement.getText()} - `, - ); - - rootLayoutSource.formatText(); - rootLayoutSource.saveSync(); -} - -function addToSafeActionClient(sourceFile?: SourceFile) { - if (!sourceFile) { - console.log(chalk.yellow("Failed to inject into safe-action-client. Did you move the safe-action.ts file?")); - return; - } - - sourceFile.addImportDeclaration({ - namedImports: [{ name: "auth" }], - moduleSpecifier: "@/server/auth", - }); - - // add to end of file - sourceFile.addStatements((writer) => - writer.writeLine(`export const authedActionClient = actionClient.use( - async ({ next, ctx }) => { - const session = await auth(); - if (!session) { - throw new Error("Unauthorized"); - } - return next({ ctx: { ...ctx, session } }); - } -); -`), - ); -} - -async function checkForNextAuthLayouts(projectDir: string) { - const existingLayouts = getExistingSchemas({ - projectDir, - dataSourceName: "filemaker", - }); - const nextAuthLayouts = ["nextauth_user", "nextauth_account", "nextauth_session", "nextauth_verificationToken"]; - - const allNextAuthLayoutsExist = nextAuthLayouts.every((layout) => - existingLayouts.some((l) => l.schemaName === layout), - ); - - if (allNextAuthLayoutsExist) { - return; - } - - const spinner = await _runExecCommand({ - command: [`next-auth-adapter-filemaker@${dependencyVersionMap["next-auth-adapter-filemaker"]}`, "install-addon"], - projectDir, - }); - - // If the spinner was used to show the progress, use succeed method on it - // If not, use the succeed on a new spinner - (spinner ?? ora()).succeed(chalk.green("Successfully installed next-auth addon for FileMaker")); - - console.log(""); - console.log(chalk.bgYellow(" ACTION REQUIRED: ")); - console.log( - `${chalk.yellowBright("You must now install the NextAuth addon in your FileMaker file.")} -Learn more: https://proofkit.proof.sh/auth/next-auth\n`, - ); -} diff --git a/packages/cli-old/src/installers/proofkit-auth.ts b/packages/cli-old/src/installers/proofkit-auth.ts deleted file mode 100644 index 89b80532..00000000 --- a/packages/cli-old/src/installers/proofkit-auth.ts +++ /dev/null @@ -1,220 +0,0 @@ -import path from "node:path"; -import type { OttoAPIKey } from "@proofkit/fmdapi"; -import chalk from "chalk"; -import dotenv from "dotenv"; -import fs from "fs-extra"; -import ora, { type Ora } from "ora"; -import { type SourceFile, SyntaxKind } from "ts-morph"; -import { getLayouts } from "~/cli/fmdapi.js"; -import * as p from "~/cli/prompts.js"; -import { abortIfCancel, UserAbortedError } from "~/cli/utils.js"; -import { PKG_ROOT } from "~/consts.js"; -import { addConfig, runCodegenCommand } from "~/generators/fmdapi.js"; -import { injectTanstackQuery } from "~/generators/tanstack-query.js"; -import { state } from "~/state.js"; -import { addPackageDependency } from "~/utils/addPackageDependency.js"; -import { getSettings } from "~/utils/parseSettings.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; -import { addToHeaderSlot } from "./auth-shared.js"; -import { installFmAddon } from "./install-fm-addon.js"; -import { installReactEmail } from "./react-email.js"; - -export const proofkitAuthInstaller = async () => { - const spinner = ora("Installing files for auth...").start(); - - const projectDir = state.projectDir; - addPackageDependency({ - projectDir, - dependencies: ["@node-rs/argon2", "@oslojs/binary", "@oslojs/crypto", "@oslojs/encoding", "js-cookie"], - devMode: false, - }); - - addPackageDependency({ - projectDir, - dependencies: ["@types/js-cookie"], - devMode: true, - }); - - // copy all files from template/extras/fmaddon-auth to projectDir/src - await fs.copy(path.join(PKG_ROOT, "template/extras/fmaddon-auth"), path.join(projectDir, "src")); - - const project = getNewProject(projectDir); - - // ensure tanstack query is installed - await injectTanstackQuery({ project }); - - // inject signin/signout components to header slots - addToHeaderSlot( - project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-right.tsx")), - "@/components/auth/user-menu", - ); - // addToHeaderSlot( - // project.addSourceFileAtPath( - // path.join( - // projectDir, - // "src/components/AppShell/slot-header-mobile-content.tsx" - // ) - // ), - // "@/components/clerk-auth/user-menu-mobile" - // ); - - addToSafeActionClient(project.addSourceFileAtPathIfExists(path.join(projectDir, "src/server/safe-action.ts"))); - - await addConfig({ - config: { - type: "fmdapi", - envNames: undefined, - clientSuffix: "Layout", - layouts: [ - { - layoutName: "proofkit_auth_sessions", - schemaName: "sessions", - strictNumbers: true, - }, - { - layoutName: "proofkit_auth_users", - schemaName: "users", - strictNumbers: true, - }, - { - layoutName: "proofkit_auth_email_verification", - schemaName: "emailVerification", - strictNumbers: true, - }, - { - layoutName: "proofkit_auth_password_reset", - schemaName: "passwordReset", - strictNumbers: true, - }, - ], - clearOldFiles: true, - validator: false, - path: "./src/server/auth/db", - }, - projectDir, - runCodegen: false, - }); - - // install email files based on the email provider in state - await installReactEmail({ project, installServerFiles: true }); - - protectMainLayout(project.addSourceFileAtPath(path.join(projectDir, "src/app/(main)/layout.tsx"))); - - await formatAndSaveSourceFiles(project); - - let hasProofKitLayouts = false; - while (!hasProofKitLayouts) { - hasProofKitLayouts = await checkForProofKitLayouts(projectDir, spinner); - - if (hasProofKitLayouts) { - spinner.text = "Successfully detected all required layouts in your FileMaker file."; - } else { - const shouldContinue = abortIfCancel( - await p.confirm({ - message: "I have followed the above instructions, continue installing", - initialValue: true, - active: "Continue", - inactive: "Abort", - }), - ); - - if (!shouldContinue) { - throw new UserAbortedError(); - } - } - } - await runCodegenCommand(); - - spinner.succeed("Auth installed successfully"); -}; - -function addToSafeActionClient(sourceFile?: SourceFile) { - if (!sourceFile) { - console.log(chalk.yellow("Failed to inject into safe-action-client. Did you move the safe-action.ts file?")); - return; - } - - sourceFile.addImportDeclaration({ - namedImports: [{ name: "getCurrentSession" }], - moduleSpecifier: "./auth/utils/session", - }); - - // add to end of file - sourceFile.addStatements((writer) => - writer.writeLine(`export const authedActionClient = actionClient.use(async ({ next, ctx }) => { - const { session, user } = await getCurrentSession(); - if (session === null) { - throw new Error("Unauthorized"); - } - - return next({ ctx: { ...ctx, session, user } }); -}); -`), - ); -} - -function protectMainLayout(sourceFile: SourceFile) { - sourceFile.addImportDeclaration({ - defaultImport: "Protect", - moduleSpecifier: "@/components/auth/protect", - }); - - // inject query provider into the root layout - - const exportDefault = sourceFile.getFunction((dec) => dec.isDefaultExport()); - const bodyElement = exportDefault - ?.getBody() - ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement) - ?.getFirstDescendantByKind(SyntaxKind.JsxElement); - - bodyElement?.replaceWithText( - ` - ${bodyElement?.getText()} - `, - ); -} - -async function checkForProofKitLayouts(projectDir: string, spinner: Ora): Promise { - const settings = getSettings(); - - const dataSource = settings.dataSources.filter((s) => s.type === "fm").find((s) => s.name === "filemaker"); - - if (!dataSource) { - return false; - } - if (settings.envFile) { - dotenv.config({ - path: path.join(projectDir, settings.envFile), - }); - } - const dataApiKey = process.env[dataSource.envNames.apiKey]; - const fmFile = process.env[dataSource.envNames.database]; - const server = process.env[dataSource.envNames.server]; - - if (!(dataApiKey && fmFile && server)) { - return false; - } - - const existingLayouts = await getLayouts({ - dataApiKey: dataApiKey as OttoAPIKey, - fmFile, - server, - }); - const proofkitAuthLayouts = [ - "proofkit_auth_sessions", - "proofkit_auth_users", - "proofkit_auth_email_verification", - "proofkit_auth_password_reset", - ]; - - const allProofkitAuthLayoutsExist = proofkitAuthLayouts.every((layout) => existingLayouts.some((l) => l === layout)); - - if (allProofkitAuthLayoutsExist) { - return true; - } - - spinner.warn("Required layouts not found"); - await installFmAddon({ addonName: "auth" }); - - return false; -} diff --git a/packages/cli-old/src/installers/proofkit-webviewer.ts b/packages/cli-old/src/installers/proofkit-webviewer.ts deleted file mode 100644 index 0635de12..00000000 --- a/packages/cli-old/src/installers/proofkit-webviewer.ts +++ /dev/null @@ -1,84 +0,0 @@ -import path from "node:path"; -import type { OttoAPIKey } from "@proofkit/fmdapi"; -import chalk from "chalk"; -import dotenv from "dotenv"; -import { getLayouts } from "~/cli/fmdapi.js"; -import * as p from "~/cli/prompts.js"; -import { abortIfCancel, UserAbortedError } from "~/cli/utils.js"; -import { state } from "~/state.js"; -import { getSettings } from "~/utils/parseSettings.js"; -import { installFmAddon } from "./install-fm-addon.js"; - -export async function checkForWebViewerLayouts(): Promise { - const settings = getSettings(); - - const dataSource = settings.dataSources - .filter((s: { type: string }) => s.type === "fm") - .find((s: { name: string; type: string }) => s.name === "filemaker") as - | { - type: "fm"; - name: string; - envNames: { database: string; server: string; apiKey: string }; - } - | undefined; - - if (!dataSource) { - return false; - } - if (settings.envFile) { - dotenv.config({ - path: path.join(state.projectDir, settings.envFile), - }); - } - const dataApiKey = process.env[dataSource.envNames.apiKey] as OttoAPIKey | undefined; - const fmFile = process.env[dataSource.envNames.database]; - const server = process.env[dataSource.envNames.server]; - - if (!(dataApiKey && fmFile && server)) { - return false; - } - - const existingLayouts = await getLayouts({ - dataApiKey, - fmFile, - server, - }); - const webviewerLayouts = ["ProofKitWV"]; - - const allWebViewerLayoutsExist = webviewerLayouts.every((layout) => - existingLayouts.some((l: string) => l === layout), - ); - - if (allWebViewerLayoutsExist) { - console.log( - chalk.green("Successfully detected all required layouts for ProofKit Web Viewer in your FileMaker file."), - ); - return true; - } - - await installFmAddon({ addonName: "wv" }); - - return false; -} - -export async function ensureWebViewerAddonInstalled() { - let hasWebViewerLayouts = false; - while (!hasWebViewerLayouts) { - hasWebViewerLayouts = await checkForWebViewerLayouts(); - - if (!hasWebViewerLayouts) { - const shouldContinue = abortIfCancel( - await p.confirm({ - message: "I have followed the above instructions, continue installing", - initialValue: true, - active: "Continue", - inactive: "Abort", - }), - ); - - if (!shouldContinue) { - throw new UserAbortedError(); - } - } - } -} diff --git a/packages/cli-old/src/installers/react-email.ts b/packages/cli-old/src/installers/react-email.ts deleted file mode 100644 index 59a90749..00000000 --- a/packages/cli-old/src/installers/react-email.ts +++ /dev/null @@ -1,211 +0,0 @@ -import path from "node:path"; -import chalk from "chalk"; -import fs from "fs-extra"; -import type { Project } from "ts-morph"; -import type { PackageJson } from "type-fest"; -import * as p from "~/cli/prompts.js"; - -import { abortIfCancel } from "~/cli/utils.js"; -import { PKG_ROOT } from "~/consts.js"; -import { installDependencies } from "~/helpers/installDependencies.js"; -import { isNonInteractiveMode, state } from "~/state.js"; -import { addPackageDependency } from "~/utils/addPackageDependency.js"; -import { addToEnv } from "~/utils/addToEnvs.js"; -import { logger } from "~/utils/logger.js"; -import { getSettings, setSettings } from "~/utils/parseSettings.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; - -export async function installReactEmail({ - ...args -}: { - project?: Project; - noInstall?: boolean; - installServerFiles?: boolean; -}) { - const projectDir = state.projectDir; - - // Exit early if already installed - const settings = getSettings(); - if (settings.ui === "shadcn") { - return false; - } - if (settings.reactEmail) { - return false; - } - - // Ensure emails directory exists - fs.ensureDirSync(path.join(projectDir, "src/emails")); - addPackageDependency({ - dependencies: ["@react-email/components", "@react-email/render"], - devMode: false, - projectDir, - }); - addPackageDependency({ - dependencies: ["react-email", "@react-email/preview-server"], - devMode: true, - projectDir, - }); - - // add a script to package.json - const pkgJson = fs.readJSONSync(path.join(projectDir, "package.json")) as PackageJson; - if (!pkgJson.scripts) { - pkgJson.scripts = {}; - } - pkgJson.scripts["email:preview"] = "email dev --port 3010 --dir=src/emails"; - fs.writeJSONSync(path.join(projectDir, "package.json"), pkgJson, { - spaces: 2, - }); - - const project = args.project ?? getNewProject(projectDir); - - if (args.installServerFiles) { - const emailProvider = state.emailProvider; - if (emailProvider === "plunk") { - await installPlunk({ project }); - } else if (emailProvider === "resend") { - await installResend({ project }); - } else { - await fs.copy( - path.join(PKG_ROOT, "template/extras/emailProviders/none/email.tsx"), - path.join(projectDir, "src/server/auth/email.tsx"), - ); - } - } - - // Copy base email template(s) into src/emails for preview and reuse - await fs.copy( - path.join(PKG_ROOT, "template/extras/emailTemplates/generic.tsx"), - path.join(projectDir, "src/emails/generic.tsx"), - ); - if (args.installServerFiles) { - await fs.copy( - path.join(PKG_ROOT, "template/extras/emailTemplates/auth-code.tsx"), - path.join(projectDir, "src/emails/auth-code.tsx"), - ); - } - - if (!args.project) { - await formatAndSaveSourceFiles(project); - } - - // Mark as installed - setSettings({ - ...settings, - reactEmail: true, - reactEmailServer: Boolean(args.installServerFiles) || settings.reactEmailServer, - }); - - // Install dependencies unless explicitly skipped - if (!args.noInstall) { - await installDependencies({ projectDir }); - } - return true; -} - -export async function installPlunk({ project }: { project?: Project }) { - const projectDir = state.projectDir; - addPackageDependency({ - dependencies: ["@plunk/node"], - devMode: false, - projectDir, - }); - - let apiKey: string; - if (typeof state.apiKey === "string") { - apiKey = state.apiKey; - } else if (isNonInteractiveMode()) { - apiKey = ""; - } else { - apiKey = abortIfCancel( - await p.text({ - message: `Enter your Plunk API key\n${chalk.dim( - "Enter your Secret API Key from https://app.useplunk.com/settings/api", - )}`, - placeholder: "...or leave blank to do this later", - }), - ); - } - - if (!apiKey) { - logger.warn("You will need to add your Plunk API key to the .env file manually for your app to run."); - } - - console.log(""); - - await addToEnv({ - projectDir, - project, - envs: [ - { - name: "PLUNK_API_KEY", - zodValue: `z.string().startsWith("sk_")`, - type: "server", - defaultValue: apiKey, - }, - ], - }); - - await fs.copy( - path.join(PKG_ROOT, "template/extras/emailProviders/plunk/service.ts"), - path.join(projectDir, "src/server/services/plunk.ts"), - ); - - await fs.copy( - path.join(PKG_ROOT, "template/extras/emailProviders/plunk/email.tsx"), - path.join(projectDir, "src/server/auth/email.tsx"), - ); -} - -export async function installResend({ project }: { project?: Project }) { - const projectDir = state.projectDir; - addPackageDependency({ - dependencies: ["resend"], - devMode: false, - projectDir, - }); - - let apiKey: string; - if (typeof state.apiKey === "string") { - apiKey = state.apiKey; - } else if (isNonInteractiveMode()) { - apiKey = ""; - } else { - apiKey = abortIfCancel( - await p.text({ - message: `Enter your Resend API key\n${chalk.dim( - `Only "Sending Access" permission required: https://resend.com/api-keys`, - )}`, - placeholder: "...or leave blank to do this later", - }), - ); - } - - if (!apiKey) { - logger.warn("You will need to add your Resend API key to the .env file manually for your app to run."); - } - - console.log(""); - - await addToEnv({ - projectDir, - project, - envs: [ - { - name: "RESEND_API_KEY", - zodValue: `z.string().startsWith("re_")`, - type: "server", - defaultValue: apiKey, - }, - ], - }); - - await fs.copy( - path.join(PKG_ROOT, "template/extras/emailProviders/resend/service.ts"), - path.join(projectDir, "src/server/services/resend.ts"), - ); - - await fs.copy( - path.join(PKG_ROOT, "template/extras/emailProviders/resend/email.tsx"), - path.join(projectDir, "src/server/auth/email.tsx"), - ); -} diff --git a/packages/cli-old/src/state.ts b/packages/cli-old/src/state.ts deleted file mode 100644 index 58711d4b..00000000 --- a/packages/cli-old/src/state.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { z } from "zod/v4"; - -const schema = z - .object({ - ci: z.boolean().default(false), - nonInteractive: z.boolean().default(false), - debug: z.boolean().default(false), - localBuild: z.boolean().default(false), - baseCommand: z.enum(["add", "init", "deploy", "upgrade", "remove"]).optional().catch(undefined), - appType: z.enum(["browser", "webviewer"]).optional().catch(undefined), - ui: z.enum(["shadcn", "mantine"]).optional().catch("mantine"), - projectDir: z.string().default(process.cwd()), - authType: z.enum(["clerk", "fmaddon"]).optional(), - emailProvider: z.enum(["plunk", "resend", "none"]).optional(), - dataSource: z.enum(["filemaker", "none"]).optional(), - }) - .passthrough(); - -type ProgramState = z.infer; -export let state: ProgramState = schema.parse({}); - -export function initProgramState(args: unknown) { - const parsed = schema.safeParse(args); - if (parsed.success) { - const mergedState = { ...state, ...parsed.data }; - const nonInteractive = mergedState.nonInteractive || mergedState.ci; - state = { ...mergedState, ci: nonInteractive, nonInteractive }; - } -} - -export function isNonInteractiveMode() { - return state.nonInteractive || state.ci; -} diff --git a/packages/cli-old/src/upgrades/cursorRules.ts b/packages/cli-old/src/upgrades/cursorRules.ts deleted file mode 100644 index 1338225f..00000000 --- a/packages/cli-old/src/upgrades/cursorRules.ts +++ /dev/null @@ -1,41 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; - -import { PKG_ROOT } from "~/consts.js"; -import { state } from "~/state.js"; -import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; - -export async function copyCursorRules() { - const projectDir = state.projectDir; - const extrasDir = path.join(PKG_ROOT, "template/extras"); - const cursorRulesSrcDir = path.join(extrasDir, "_cursor/rules"); - const cursorRulesDestDir = path.join(projectDir, ".cursor/rules"); - - if (!fs.existsSync(cursorRulesSrcDir)) { - return; - } - - const pkgManager = getUserPkgManager(); - await fs.ensureDir(cursorRulesDestDir); - await fs.copy(cursorRulesSrcDir, cursorRulesDestDir); - - // Copy package manager specific rules - const conditionalRulesDir = path.join(extrasDir, "_cursor/conditional-rules"); - - const packageManagerRules = { - pnpm: "pnpm.mdc", - npm: "npm.mdc", - yarn: "yarn.mdc", - }; - - const selectedRule = packageManagerRules[pkgManager as keyof typeof packageManagerRules]; - - if (selectedRule) { - const ruleSrc = path.join(conditionalRulesDir, selectedRule); - const ruleDest = path.join(cursorRulesDestDir, "package-manager.mdc"); - - if (fs.existsSync(ruleSrc)) { - await fs.copy(ruleSrc, ruleDest, { overwrite: true }); - } - } -} diff --git a/packages/cli-old/src/upgrades/index.ts b/packages/cli-old/src/upgrades/index.ts deleted file mode 100644 index b72dbc98..00000000 --- a/packages/cli-old/src/upgrades/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { type appTypes, getSettings, mergeSettings } from "~/utils/parseSettings.js"; -import { copyCursorRules } from "./cursorRules.js"; -import { addShadcn } from "./shadcn.js"; - -interface Upgrade { - key: string; - title: string; - description: string; - appType: (typeof appTypes)[number][]; - function: () => Promise; -} - -const availableUpgrades: Upgrade[] = [ - { - key: "cursorRules", - title: "Upgrade Cursor Rules", - description: "Upgrade the .cursor rules in your project to the latest version.", - appType: ["browser"], - function: copyCursorRules, - }, - { - key: "shadcn", - title: "Add Shadcn", - description: - "Add Shadcn to your project, to support easily adding new components from a variety of component registries.", - appType: ["browser", "webviewer"], - function: addShadcn, - }, -]; - -export type UpgradeKeys = (typeof availableUpgrades)[number]["key"]; - -export function checkForAvailableUpgrades() { - const settings = getSettings(); - if (settings.ui === "shadcn") { - return []; - } - - const appliedUpgrades = settings.appliedUpgrades; - - const neededUpgrades = availableUpgrades.filter( - (upgrade) => !appliedUpgrades.includes(upgrade.key) && upgrade.appType.includes(settings.appType), - ); - - return neededUpgrades.map(({ key, title, description }) => ({ - key, - title, - description, - })); -} - -export async function runAllAvailableUpgrades() { - const upgrades = checkForAvailableUpgrades(); - const settings = getSettings(); - if (settings.ui === "shadcn") { - return; - } - - for (const upgrade of upgrades) { - const upgradeFunction = availableUpgrades.find((u) => u.key === upgrade.key)?.function; - if (upgradeFunction) { - await upgradeFunction(); - const appliedUpgrades = settings.appliedUpgrades; - mergeSettings({ - appliedUpgrades: [...appliedUpgrades, upgrade.key], - }); - } - } -} diff --git a/packages/cli-old/src/upgrades/shadcn.ts b/packages/cli-old/src/upgrades/shadcn.ts deleted file mode 100644 index d385a4d0..00000000 --- a/packages/cli-old/src/upgrades/shadcn.ts +++ /dev/null @@ -1,53 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; - -import { PKG_ROOT } from "~/consts.js"; -import { installDependencies } from "~/helpers/installDependencies.js"; -import type { AvailableDependencies } from "~/installers/dependencyVersionMap.js"; -import { state } from "~/state.js"; -import { addPackageDependency } from "~/utils/addPackageDependency.js"; - -const BASE_DEPS = [ - "@radix-ui/react-slot", - "@tailwindcss/postcss", - "class-variance-authority", - "clsx", - "lucide-react", - "tailwind-merge", - "tailwindcss", - "tw-animate-css", -] as AvailableDependencies[]; -const BASE_DEV_DEPS = [] as AvailableDependencies[]; - -export async function addShadcn() { - const projectDir = state.projectDir; - - const TEMPLATE_ROOT = path.join(PKG_ROOT, "template/nextjs"); - - // 1. Add dependencies - addPackageDependency({ - dependencies: BASE_DEPS, - devMode: false, - projectDir, - }); - addPackageDependency({ - dependencies: BASE_DEV_DEPS, - devMode: true, - projectDir, - }); - - // 2. Copy config and utility files - fs.copySync(path.join(TEMPLATE_ROOT, "components.json"), path.join(projectDir, "components.json")); - fs.copySync(path.join(TEMPLATE_ROOT, "postcss.config.cjs"), path.join(projectDir, "postcss.config.cjs")); - fs.copySync(path.join(TEMPLATE_ROOT, "src/utils/styles.ts"), path.join(projectDir, "src/utils/styles.ts")); - fs.copySync( - path.join(TEMPLATE_ROOT, "src/config/theme/globals.css"), - path.join(projectDir, "src/config/theme/globals.css"), - ); - - // 3. Install dependencies - await installDependencies(); - - // 4. Success message - console.log("\nโœ… shadcn/ui + Tailwind v4 upgrade complete!\n"); -} diff --git a/packages/cli-old/src/utils/addPackageDependency.ts b/packages/cli-old/src/utils/addPackageDependency.ts deleted file mode 100644 index c2d139e7..00000000 --- a/packages/cli-old/src/utils/addPackageDependency.ts +++ /dev/null @@ -1,32 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; -import sortPackageJson from "sort-package-json"; -import type { PackageJson } from "type-fest"; - -import { type AvailableDependencies, dependencyVersionMap } from "~/installers/dependencyVersionMap.js"; -import { state } from "~/state.js"; - -export const addPackageDependency = (opts: { - dependencies: AvailableDependencies[]; - devMode: boolean; - projectDir?: string; -}) => { - const { dependencies, devMode, projectDir = state.projectDir } = opts; - - const pkgJson = fs.readJSONSync(path.join(projectDir, "package.json")) as PackageJson; - - for (const pkgName of dependencies) { - const version = dependencyVersionMap[pkgName]; - - if (devMode && pkgJson.devDependencies) { - pkgJson.devDependencies[pkgName] = version; - } else if (pkgJson.dependencies) { - pkgJson.dependencies[pkgName] = version; - } - } - const sortedPkgJson = sortPackageJson(pkgJson); - - fs.writeJSONSync(path.join(projectDir, "package.json"), sortedPkgJson, { - spaces: 2, - }); -}; diff --git a/packages/cli-old/src/utils/addToEnvs.ts b/packages/cli-old/src/utils/addToEnvs.ts deleted file mode 100644 index 5af7e131..00000000 --- a/packages/cli-old/src/utils/addToEnvs.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { execSync } from "node:child_process"; -import path from "node:path"; -import fs from "fs-extra"; -import { type Project, SyntaxKind } from "ts-morph"; - -import { findT3EnvFile } from "~/installers/envVars.js"; -import { state } from "~/state.js"; -import { formatAndSaveSourceFiles, getNewProject } from "./ts-morph.js"; - -interface EnvSchema { - name: string; - zodValue: string; - /** This value will be added to the .env file, unless `addToRuntimeEnv` is set to `false`. */ - defaultValue?: string; - type: "server" | "client"; - addToRuntimeEnv?: boolean; -} - -export async function addToEnv({ - projectDir = state.projectDir, - envs, - envFileDescription, - ...args -}: { - projectDir?: string; - project?: Project; - envs: EnvSchema[]; - envFileDescription?: string; -}) { - const envSchemaFile = findT3EnvFile(); - - const project = args.project ?? getNewProject(projectDir); - const schemaFile = project.addSourceFileAtPath(envSchemaFile); - - if (!schemaFile) { - throw new Error("Schema file not found"); - } - - // Find the createEnv call expression - const createEnvCall = schemaFile - .getDescendantsOfKind(SyntaxKind.CallExpression) - .find((callExpr) => callExpr.getExpression().getText() === "createEnv"); - - if (!createEnvCall) { - throw new Error( - "Could not find createEnv call in schema file. Make sure you have a valid env.ts file with createEnv setup.", - ); - } - - // Get the server object property - const opts = createEnvCall.getArguments()[0]; - if (!opts) { - throw new Error("createEnv call is missing options argument"); - } - - const serverProperty = opts - .getDescendantsOfKind(SyntaxKind.PropertyAssignment) - .find((prop) => prop.getName() === "server") - ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression); - - const clientProperty = opts - .getDescendantsOfKind(SyntaxKind.PropertyAssignment) - .find((prop) => prop.getName() === "client") - ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression); - - const runtimeEnvProperty = opts - .getDescendantsOfKind(SyntaxKind.PropertyAssignment) - .find((prop) => prop.getName() === "experimental__runtimeEnv") - ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression); - - const serverEnvs = envs.filter((env) => env.type === "server"); - const clientEnvs = envs.filter((env) => env.type === "client"); - - for (const env of serverEnvs) { - serverProperty?.addPropertyAssignment({ - name: env.name, - initializer: env.zodValue, - }); - } - - for (const env of clientEnvs) { - clientProperty?.addPropertyAssignment({ - name: env.name, - initializer: env.zodValue, - }); - - runtimeEnvProperty?.addPropertyAssignment({ - name: env.name, - initializer: `process.env.${env.name}`, - }); - } - - const envsString = envs - .filter((env) => env.addToRuntimeEnv ?? true) - .map((env) => `${env.name}=${env.defaultValue ?? ""}`) - .join("\n"); - - const dotEnvFile = path.join(projectDir, ".env"); - - // Only handle .env file if it already exists - if (fs.existsSync(dotEnvFile)) { - const currentFile = fs.readFileSync(dotEnvFile, "utf-8"); - - // Ensure .env is in .gitignore using command line - const gitIgnoreFile = path.join(projectDir, ".gitignore"); - try { - let gitIgnoreContent = ""; - if (fs.existsSync(gitIgnoreFile)) { - gitIgnoreContent = fs.readFileSync(gitIgnoreFile, "utf-8"); - } - - if (!gitIgnoreContent.includes(".env")) { - execSync(`echo ".env" >> "${gitIgnoreFile}"`, { cwd: projectDir }); - } - } catch (_error) { - // Silently ignore gitignore errors - } - - const newContent = `${currentFile} -${envFileDescription ? `# ${envFileDescription}\n${envsString}` : envsString} - `; - - fs.writeFileSync(dotEnvFile, newContent); - } - - if (!args.project) { - await formatAndSaveSourceFiles(project); - } - - return schemaFile; -} diff --git a/packages/cli-old/src/utils/formatting.ts b/packages/cli-old/src/utils/formatting.ts deleted file mode 100644 index 8522e45a..00000000 --- a/packages/cli-old/src/utils/formatting.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { execa } from "execa"; -import type { Project } from "ts-morph"; - -import { state } from "~/state.js"; - -/** - * Formats all source files in a ts-morph Project using biome and saves the changes. - * @param project The ts-morph Project containing the files to format - */ -export async function formatAndSaveSourceFiles(project: Project) { - await project.save(); // save files first - try { - // Run biome format on the project directory - await execa("npx", ["@biomejs/biome", "format", "--write", state.projectDir], { - cwd: state.projectDir, - }); - } catch (error) { - if (state.debug) { - console.log("Error formatting files with biome"); - console.error(error); - } - // Continue even if formatting fails - } -} diff --git a/packages/cli-old/src/utils/getProofKitVersion.ts b/packages/cli-old/src/utils/getProofKitVersion.ts deleted file mode 100644 index 496e46a2..00000000 --- a/packages/cli-old/src/utils/getProofKitVersion.ts +++ /dev/null @@ -1,38 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; -import type { PackageJson } from "type-fest"; - -import { PKG_ROOT } from "~/consts.js"; - -export const getVersion = () => { - const packageJsonPath = path.join(PKG_ROOT, "package.json"); - - const packageJsonContent = fs.readJSONSync(packageJsonPath) as PackageJson; - - return packageJsonContent.version ?? "1.0.0"; -}; - -export const getFmdapiVersion = () => { - return __FMDAPI_VERSION__; -}; - -export const getNodeMajorVersion = () => { - const defaultVersion = "22"; - try { - return process.versions.node.split(".")[0] ?? defaultVersion; - } catch { - return defaultVersion; - } -}; - -export const getProofkitBetterAuthVersion = () => { - return __BETTER_AUTH_VERSION__; -}; - -export const getProofkitWebviewerVersion = () => { - return __WEBVIEWER_VERSION__; -}; - -export const getTypegenVersion = () => { - return __TYPEGEN_VERSION__; -}; diff --git a/packages/cli-old/src/utils/getUserPkgManager.ts b/packages/cli-old/src/utils/getUserPkgManager.ts deleted file mode 100644 index d2e3afdc..00000000 --- a/packages/cli-old/src/utils/getUserPkgManager.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type PackageManager = "npm" | "pnpm" | "yarn" | "bun"; - -export const getUserPkgManager: () => PackageManager = () => { - // This environment variable is set by npm and yarn but pnpm seems less consistent - const userAgent = process.env.npm_config_user_agent; - - if (userAgent) { - if (userAgent.startsWith("yarn")) { - return "yarn"; - } - if (userAgent.startsWith("pnpm")) { - return "pnpm"; - } - if (userAgent.startsWith("bun")) { - return "bun"; - } - return "npm"; - } - // If no user agent is set, assume pnpm - return "pnpm"; -}; diff --git a/packages/cli-old/src/utils/isTTYError.ts b/packages/cli-old/src/utils/isTTYError.ts deleted file mode 100644 index ccf602ed..00000000 --- a/packages/cli-old/src/utils/isTTYError.ts +++ /dev/null @@ -1 +0,0 @@ -export class IsTTYError extends Error {} diff --git a/packages/cli-old/src/utils/logger.ts b/packages/cli-old/src/utils/logger.ts deleted file mode 100644 index 3ddb9775..00000000 --- a/packages/cli-old/src/utils/logger.ts +++ /dev/null @@ -1,19 +0,0 @@ -import chalk from "chalk"; - -export const logger = { - error(...args: unknown[]) { - console.log(chalk.red(...args)); - }, - warn(...args: unknown[]) { - console.log(chalk.yellow(...args)); - }, - info(...args: unknown[]) { - console.log(chalk.cyan(...args)); - }, - success(...args: unknown[]) { - console.log(chalk.green(...args)); - }, - dim(...args: unknown[]) { - console.log(chalk.dim(...args)); - }, -}; diff --git a/packages/cli-old/src/utils/parseNameAndPath.ts b/packages/cli-old/src/utils/parseNameAndPath.ts deleted file mode 100644 index a4d4507e..00000000 --- a/packages/cli-old/src/utils/parseNameAndPath.ts +++ /dev/null @@ -1,42 +0,0 @@ -import pathModule from "node:path"; - -import { removeTrailingSlash } from "./removeTrailingSlash.js"; - -/** - * Parses the appName and its path from the user input. - * - * Returns a tuple of of `[appName, path]`, where `appName` is the name put in the "package.json" - * file and `path` is the path to the directory where the app will be created. - * - * If `appName` is ".", the name of the directory will be used instead. Handles the case where the - * input includes a scoped package name in which case that is being parsed as the name, but not - * included as the path. - * - * For example: - * - * - dir/@mono/app => ["@mono/app", "dir/app"] - * - dir/app => ["app", "dir/app"] - */ -export const parseNameAndPath = (rawInput: string) => { - const input = removeTrailingSlash(rawInput); - - const paths = input.split("/"); - - let appName = paths.at(-1) ?? ""; - - // If the user ran `npx proofkit .` or similar, the appName should be the current directory - if (appName === ".") { - const parsedCwd = pathModule.resolve(process.cwd()); - appName = pathModule.basename(parsedCwd); - } - - // If the first part is a @, it's a scoped package - const indexOfDelimiter = paths.findIndex((p) => p.startsWith("@")); - if (paths.findIndex((p) => p.startsWith("@")) !== -1) { - appName = paths.slice(indexOfDelimiter).join("/"); - } - - const path = paths.filter((p) => !p.startsWith("@")).join("/"); - - return [appName, path] as const; -}; diff --git a/packages/cli-old/src/utils/parseSettings.ts b/packages/cli-old/src/utils/parseSettings.ts deleted file mode 100644 index eb77a8ec..00000000 --- a/packages/cli-old/src/utils/parseSettings.ts +++ /dev/null @@ -1,153 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; -import { z } from "zod/v4"; - -import { state } from "~/state.js"; - -const authSchema = z - .discriminatedUnion("type", [ - z.object({ - type: z.literal("clerk"), - }), - z.object({ - type: z.literal("next-auth"), - }), - z.object({ - type: z.literal("proofkit").transform(() => "fmaddon"), - }), - z.object({ - type: z.literal("fmaddon"), - }), - z.object({ - type: z.literal("better-auth"), - }), - z.object({ - type: z.literal("none"), - }), - ]) - .default({ type: "none" }); - -export const envNamesSchema = z.object({ - database: z.string().default("FM_DATABASE"), - server: z.string().default("FM_SERVER"), - apiKey: z.string().default("OTTO_API_KEY"), -}); -export const dataSourceSchema = z.discriminatedUnion("type", [ - z.object({ - type: z.literal("fm"), - name: z.string(), - envNames: envNamesSchema, - }), - z.object({ - type: z.literal("supabase"), - name: z.string(), - }), -]); -export type DataSource = z.infer; - -export const appTypes = ["browser", "webviewer"] as const; - -export const uiTypes = ["shadcn", "mantine"] as const; -export type Ui = (typeof uiTypes)[number]; - -const settingsSchema = z.discriminatedUnion("ui", [ - z.object({ - ui: z.literal("mantine"), - appType: z.enum(appTypes).default("browser"), - auth: authSchema, - envFile: z.string().optional(), - dataSources: z.array(dataSourceSchema).default([]), - tanstackQuery: z.boolean().catch(false), - replacedMainPage: z.boolean().catch(false), - // Whether React Email scaffolding has been installed - reactEmail: z.boolean().catch(false), - // Whether provider-specific server email sender files have been installed - reactEmailServer: z.boolean().catch(false), - appliedUpgrades: z.array(z.string()).default([]), - registryUrl: z.url().optional(), - registryTemplates: z.array(z.string()).default([]), - }), - z.object({ - ui: z.literal("shadcn"), - appType: z.enum(appTypes).default("browser"), - envFile: z.string().optional(), - dataSources: z.array(dataSourceSchema).default([]), - replacedMainPage: z.boolean().catch(false), - registryUrl: z.url().optional(), - registryTemplates: z.array(z.string()).default([]), - }), -]); - -export const defaultSettings = settingsSchema.parse({ - auth: { type: "none" }, - ui: "shadcn", - appType: "browser", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], -}); - -let settings: Settings | undefined; -export const getSettings = () => { - if (settings) { - return settings; - } - - const settingsPath = path.join(state.projectDir, "proofkit.json"); - - // Check if the settings file exists before trying to read it - if (!fs.existsSync(settingsPath)) { - throw new Error(`ProofKit settings file not found at: ${settingsPath}`); - } - - let settingsFile: unknown = fs.readJSONSync(settingsPath); - - if (typeof settingsFile === "object" && settingsFile !== null && !("ui" in settingsFile)) { - settingsFile = { ...settingsFile, ui: "mantine" }; - } - - const parsed = settingsSchema.parse(settingsFile); - - state.appType = parsed.appType; - return parsed; -}; - -export type Settings = z.infer; - -export function mergeSettings(_settings: Partial) { - const settings = getSettings(); - const merged = { ...settings, ..._settings }; - const validated = settingsSchema.parse(merged); - setSettings(validated); -} - -export function setSettings(_settings: Settings) { - fs.writeJSONSync(path.join(state.projectDir, "proofkit.json"), _settings, { - spaces: 2, - }); - settings = _settings; - return settings; -} - -/** - * Validates and sets the envFile in settings only if the file exists. - * Used during stealth initialization to avoid setting non-existent env files. - */ -export function validateAndSetEnvFile(envFileName = ".env") { - const settings = getSettings(); - const envFilePath = path.join(state.projectDir, envFileName); - - if (fs.existsSync(envFilePath)) { - const updatedSettings = { ...settings, envFile: envFileName }; - setSettings(updatedSettings); - return envFileName; - } - - // If no env file exists, ensure envFile is undefined in settings - if (settings.envFile) { - const { envFile, ...settingsWithoutEnvFile } = settings; - setSettings(settingsWithoutEnvFile as Settings); - } - - return undefined; -} diff --git a/packages/cli-old/src/utils/proofkitReleaseChannel.ts b/packages/cli-old/src/utils/proofkitReleaseChannel.ts deleted file mode 100644 index aa7ecf17..00000000 --- a/packages/cli-old/src/utils/proofkitReleaseChannel.ts +++ /dev/null @@ -1,93 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; -import semver from "semver"; - -import { - getFmdapiVersion, - getProofkitBetterAuthVersion, - getProofkitWebviewerVersion, - getTypegenVersion, - getVersion, -} from "~/utils/getProofKitVersion.js"; - -export type ProofkitReleaseTag = "latest" | "beta"; - -interface ChangesetPreState { - mode?: string; - tag?: string; -} - -function findRepoRootWithChangeset(startDir: string): string | null { - let currentDir = path.resolve(startDir); - const { root } = path.parse(currentDir); - - while (currentDir !== root) { - if (fs.existsSync(path.join(currentDir, ".changeset"))) { - return currentDir; - } - currentDir = path.dirname(currentDir); - } - - return null; -} - -function readChangesetPreState(startDir = process.cwd()): ChangesetPreState | null { - const repoRoot = findRepoRootWithChangeset(startDir); - if (!repoRoot) { - return null; - } - - const prePath = path.join(repoRoot, ".changeset", "pre.json"); - if (!fs.existsSync(prePath)) { - return null; - } - - try { - return fs.readJSONSync(prePath) as ChangesetPreState; - } catch { - return null; - } -} - -export function hasAnyPrereleaseVersion(versionCandidates?: Array) { - if (versionCandidates) { - return versionCandidates.some((version) => { - if (!version) { - return false; - } - return semver.valid(version) && semver.prerelease(version); - }); - } - - const readVersion = (getter: () => string) => { - try { - return getter(); - } catch { - return null; - } - }; - - const proofkitVersions = [ - readVersion(getVersion), - readVersion(getFmdapiVersion), - readVersion(getProofkitWebviewerVersion), - readVersion(getTypegenVersion), - readVersion(getProofkitBetterAuthVersion), - ].filter((version): version is string => Boolean(version)); - - return proofkitVersions.some((version) => semver.valid(version) && semver.prerelease(version)); -} - -export function getProofkitReleaseTag(startDir = process.cwd()): ProofkitReleaseTag { - const preState = readChangesetPreState(startDir); - - if (preState?.mode === "pre" && preState.tag === "beta") { - return "beta"; - } - - if (hasAnyPrereleaseVersion()) { - return "beta"; - } - - return "latest"; -} diff --git a/packages/cli-old/src/utils/removeTrailingSlash.ts b/packages/cli-old/src/utils/removeTrailingSlash.ts deleted file mode 100644 index 051c3322..00000000 --- a/packages/cli-old/src/utils/removeTrailingSlash.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const removeTrailingSlash = (input: string) => { - if (input.length > 1 && input.endsWith("/")) { - return input.slice(0, -1); - } - return input; -}; diff --git a/packages/cli-old/src/utils/renderTitle.ts b/packages/cli-old/src/utils/renderTitle.ts deleted file mode 100644 index d0f89738..00000000 --- a/packages/cli-old/src/utils/renderTitle.ts +++ /dev/null @@ -1,20 +0,0 @@ -import gradient from "gradient-string"; - -import { TITLE_TEXT } from "~/consts.js"; -import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; - -const proofTheme = { - purple: "#89216B", - lightPurple: "#D15ABB", - orange: "#FF595E", -}; - -export const proofGradient = gradient(Object.values(proofTheme)); -export const renderTitle = () => { - // resolves weird behavior where the ascii is offset - const pkgManager = getUserPkgManager(); - if (pkgManager === "yarn" || pkgManager === "pnpm") { - console.log(""); - } - console.log(proofGradient.multiline(TITLE_TEXT)); -}; diff --git a/packages/cli-old/src/utils/renderVersionWarning.ts b/packages/cli-old/src/utils/renderVersionWarning.ts deleted file mode 100644 index fd046831..00000000 --- a/packages/cli-old/src/utils/renderVersionWarning.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { execSync } from "node:child_process"; -import https from "node:https"; -import chalk from "chalk"; -import * as semver from "semver"; -import * as p from "~/cli/prompts.js"; - -import { cliName, npmName } from "~/consts.js"; -import { getVersion } from "./getProofKitVersion.js"; -import { getUserPkgManager } from "./getUserPkgManager.js"; -import { logger } from "./logger.js"; - -export const renderVersionWarning = (npmVersion: string) => { - const currentVersion = getVersion(); - - // Check if current version is a pre-release (beta, alpha, etc.) - if (semver.prerelease(currentVersion)) { - logger.warn(` You are using a pre-release version of ${cliName}.`); - logger.warn(" Please report any bugs you encounter."); - } else if (semver.valid(currentVersion) && semver.valid(npmVersion) && semver.lt(currentVersion, npmVersion)) { - logger.warn(` You are using an outdated version of ${cliName}.`); - logger.warn(" Your version:", `${currentVersion}.`, "Latest version in the npm registry:", npmVersion); - logger.warn(" Please run the CLI with @latest to get the latest updates."); - } - console.log(""); -}; - -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the LICENSE file in the root - * directory of this source tree. - * https://github.com/facebook/create-react-app/blob/main/packages/create-react-app/LICENSE - */ -interface DistTagsBody { - latest: string; -} - -function checkForLatestVersion(): Promise { - return new Promise((resolve, reject) => { - https - .get("https://registry.npmjs.org/-/package/@proofkit/cli/dist-tags", (res) => { - if (res.statusCode === 200) { - let body = ""; - res.on("data", (data) => { - body += data; - }); - res.on("end", () => { - resolve((JSON.parse(body) as DistTagsBody).latest); - }); - } else { - reject(); - } - }) - .on("error", () => { - // logger.error("Unable to check for latest version."); - reject(); - }); - }); -} - -export const getNpmVersion = async () => - // `fetch` to the registry is faster than `npm view` so we try that first - checkForLatestVersion().catch(() => { - try { - return execSync("npm view proofkit version").toString().trim(); - } catch { - return null; - } - }); - -export const checkAndRenderVersionWarning = async () => { - const npmVersion = await getNpmVersion(); - const currentVersion = getVersion(); - - // Only show warning if current version is valid, npm version is valid, and current is actually older - if (npmVersion && semver.valid(currentVersion) && semver.valid(npmVersion) && semver.lt(currentVersion, npmVersion)) { - const pkgManager = getUserPkgManager(); - p.log.warn( - `${chalk.yellow( - `You are using an outdated version of ${cliName}.`, - )} Your version: ${currentVersion}. Latest version: ${npmVersion}. - Run ${chalk.magenta.bold(`${pkgManager} install ${npmName}@latest`)} to get the latest updates.`, - ); - } - return { npmVersion, currentVersion }; -}; diff --git a/packages/cli-old/src/utils/ts-morph.ts b/packages/cli-old/src/utils/ts-morph.ts deleted file mode 100644 index 92d58954..00000000 --- a/packages/cli-old/src/utils/ts-morph.ts +++ /dev/null @@ -1,25 +0,0 @@ -import path from "node:path"; -import { Project, type ReturnStatement, SyntaxKind } from "ts-morph"; - -export { formatAndSaveSourceFiles } from "./formatting.js"; - -export function ensureReturnStatementIsWrappedInFragment(returnStatement: ReturnStatement | undefined) { - const expression = - returnStatement?.getExpressionIfKind(SyntaxKind.ParenthesizedExpression)?.getExpression() ?? - returnStatement?.getExpression(); - - if (expression?.isKind(SyntaxKind.JsxFragment)) { - return returnStatement; - } - - returnStatement?.replaceWithText(`return <>${expression};`); - return returnStatement; -} - -export function getNewProject(projectDir?: string) { - const project = new Project({ - tsConfigFilePath: path.join(projectDir ?? process.cwd(), "tsconfig.json"), - }); - - return project; -} diff --git a/packages/cli-old/src/utils/validateAppName.ts b/packages/cli-old/src/utils/validateAppName.ts deleted file mode 100644 index b5b4e42e..00000000 --- a/packages/cli-old/src/utils/validateAppName.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { removeTrailingSlash } from "./removeTrailingSlash.js"; - -const validationRegExp = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/; - -//Validate a string against allowed package.json names -export const validateAppName = (rawInput: string) => { - const input = removeTrailingSlash(rawInput); - const paths = input.split("/"); - - // If the first part is a @, it's a scoped package - const indexOfDelimiter = paths.findIndex((p) => p.startsWith("@")); - - let appName = paths.at(-1); - if (paths.findIndex((p) => p.startsWith("@")) !== -1) { - appName = paths.slice(indexOfDelimiter).join("/"); - } - - if (input === "." || validationRegExp.test(appName ?? "")) { - return; - } - return "Name must consist of only lowercase alphanumeric characters, '-', and '_'"; -}; diff --git a/packages/cli-old/src/utils/validateImportAlias.ts b/packages/cli-old/src/utils/validateImportAlias.ts deleted file mode 100644 index bd33ca61..00000000 --- a/packages/cli-old/src/utils/validateImportAlias.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const validateImportAlias = (input: string) => { - if (input.startsWith(".") || input.startsWith("/")) { - return "Import alias can't start with '.' or '/'"; - } - return; -}; diff --git a/packages/cli-old/template/extras/_cursor/conditional-rules/nextjs-framework.mdc b/packages/cli-old/template/extras/_cursor/conditional-rules/nextjs-framework.mdc deleted file mode 100644 index 5ce7a9e0..00000000 --- a/packages/cli-old/template/extras/_cursor/conditional-rules/nextjs-framework.mdc +++ /dev/null @@ -1,51 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -# Next.js Framework Configuration - -This rule documents the Next.js framework setup and conventions used in this project. - - -name: nextjs_framework -description: Documents Next.js framework setup, routing conventions, and best practices -filters: - - type: file_extension - pattern: "\\.(ts|tsx)$" - - type: directory - pattern: "src/(app|components)/" - -conventions: - routing: - - App Router is used (not Pages Router) - - Routes are defined in src/app directory - - Layout components should be named layout.tsx - - Page components should be named page.tsx - - Loading states should be in loading.tsx - - Error boundaries should be in error.tsx - - components: - - React Server Components (RSC) are default - - Client components must be marked with "use client" - - Components live in src/components/ - - Shared layouts in src/components/layouts/ - - UI components in src/components/ui/ - - data_fetching: - - Server components fetch data directly - - Client components use React Query - - API routes defined in src/app/api/ - - Server actions used for mutations - -frameworks: - next: "15.1.7" - react: "19.0.0-rc" - typescript: "^5" - mantine: "^7.17.0" - tanstack_query: "^5.59.0" - -metadata: - priority: high - version: 1.0 - \ No newline at end of file diff --git a/packages/cli-old/template/extras/_cursor/conditional-rules/npm.mdc b/packages/cli-old/template/extras/_cursor/conditional-rules/npm.mdc deleted file mode 100644 index 3b030fa5..00000000 --- a/packages/cli-old/template/extras/_cursor/conditional-rules/npm.mdc +++ /dev/null @@ -1,60 +0,0 @@ ---- -description: | - This rule documents the package manager configuration and usage. It should be included when: - 1. Installing dependencies - 2. Running scripts - 3. Managing project packages - 4. Running development commands - 5. Executing build or test operations -globs: - - "package.json" - - "package-lock.json" - - ".npmrc" -alwaysApply: true ---- -# Package Manager Configuration - -This rule documents the package manager setup and usage requirements. - - -name: package_manager -description: Documents package manager configuration and usage requirements - -configuration: - name: "npm" - version: "latest" - commands: - install: "npm install" - build: "npm run build" - dev: "npm run dev" - typegen: "npm run typegen" - typecheck: "npm run tsc" - notes: "Always use npm instead of yarn or pnpm for consistency" - dev_server_guidelines: - - "Never relaunch the dev server command if it may already be running" - - "Use npm run dev only when explicitly needed to start the server for the first time" - - "For code changes, just save the files and the server will automatically reload" - -examples: - - description: "Installing dependencies" - correct: "npm install" - incorrect: - - "pnpm install" - - "yarn install" - - - description: "Running scripts" - correct: "npm run script-name" - incorrect: - - "pnpm run script-name" - - "yarn script-name" - - - description: "Adding dependencies" - correct: "npm install package-name" - incorrect: - - "pnpm add package-name" - - "yarn add package-name" - -metadata: - priority: high - version: 1.0 - \ No newline at end of file diff --git a/packages/cli-old/template/extras/_cursor/conditional-rules/pnpm.mdc b/packages/cli-old/template/extras/_cursor/conditional-rules/pnpm.mdc deleted file mode 100644 index d25da047..00000000 --- a/packages/cli-old/template/extras/_cursor/conditional-rules/pnpm.mdc +++ /dev/null @@ -1,65 +0,0 @@ ---- -description: | -globs: -alwaysApply: true ---- ---- -description: | - This rule documents the package manager configuration and usage. It should be included when: - 1. Installing dependencies - 2. Running scripts - 3. Managing project packages - 4. Running development commands - 5. Executing build or test operations -globs: - - "package.json" - - "pnpm-lock.yaml" - - ".npmrc" -alwaysApply: true ---- -# Package Manager Configuration - -This rule documents the package manager setup and usage requirements. - - -name: package_manager -description: Documents package manager configuration and usage requirements - -configuration: - name: "pnpm" - version: "latest" - commands: - install: "pnpm install" - build: "pnpm build" - dev: "pnpm dev" - typegen: "pnpm typegen" - typecheck: "pnpm tsc" - notes: "Always use pnpm instead of npm or yarn for consistency" - dev_server_guidelines: - - "Never relaunch the dev server command if it may already be running" - - "Use pnpm dev only when explicitly needed to start the server for the first time" - - "For code changes, just save the files and the server will automatically reload" - -examples: - - description: "Installing dependencies" - correct: "pnpm install" - incorrect: - - "npm install" - - "yarn install" - - - description: "Running scripts" - correct: "pnpm run script-name" - incorrect: - - "npm run script-name" - - "yarn script-name" - - - description: "Adding dependencies" - correct: "pnpm add package-name" - incorrect: - - "npm install package-name" - - "yarn add package-name" - -metadata: - priority: high - version: 1.0 - \ No newline at end of file diff --git a/packages/cli-old/template/extras/_cursor/conditional-rules/yarn.mdc b/packages/cli-old/template/extras/_cursor/conditional-rules/yarn.mdc deleted file mode 100644 index 5672e80e..00000000 --- a/packages/cli-old/template/extras/_cursor/conditional-rules/yarn.mdc +++ /dev/null @@ -1,60 +0,0 @@ ---- -description: | - This rule documents the package manager configuration and usage. It should be included when: - 1. Installing dependencies - 2. Running scripts - 3. Managing project packages - 4. Running development commands - 5. Executing build or test operations -globs: - - "package.json" - - "yarn.lock" - - ".yarnrc" -alwaysApply: true ---- -# Package Manager Configuration - -This rule documents the package manager setup and usage requirements. - - -name: package_manager -description: Documents package manager configuration and usage requirements - -configuration: - name: "yarn" - version: "latest" - commands: - install: "yarn install" - build: "yarn build" - dev: "yarn dev" - typegen: "yarn typegen" - typecheck: "yarn tsc" - notes: "Always use yarn instead of npm or pnpm for consistency" - dev_server_guidelines: - - "Never relaunch the dev server command if it may already be running" - - "Use yarn dev only when explicitly needed to start the server for the first time" - - "For code changes, just save the files and the server will automatically reload" - -examples: - - description: "Installing dependencies" - correct: "yarn install" - incorrect: - - "npm install" - - "pnpm install" - - - description: "Running scripts" - correct: "yarn script-name" - incorrect: - - "npm run script-name" - - "pnpm run script-name" - - - description: "Adding dependencies" - correct: "yarn add package-name" - incorrect: - - "npm install package-name" - - "pnpm add package-name" - -metadata: - priority: high - version: 1.0 - \ No newline at end of file diff --git a/packages/cli-old/template/extras/_cursor/rules/cursor-rules.mdc b/packages/cli-old/template/extras/_cursor/rules/cursor-rules.mdc deleted file mode 100644 index 061da499..00000000 --- a/packages/cli-old/template/extras/_cursor/rules/cursor-rules.mdc +++ /dev/null @@ -1,88 +0,0 @@ ---- -description: | - This rule documents how to manage and organize Cursor rules. It should be included when: - 1. Creating or modifying Cursor rules - 2. Organizing documentation for the codebase - 3. Setting up new development patterns - 4. Adding project-wide conventions - 5. Managing rule file locations - 6. Updating rule descriptions or globs - 7. Working with .cursor directory structure -globs: - - ".cursor/rules/*.mdc" - - ".cursor/config/*.json" - - ".cursor/settings/*.json" -alwaysApply: true ---- -# Cursor Rules Location - -Rules for placing and organizing Cursor rule files in the repository. - - -name: cursor_rules_location -description: Standards for placing Cursor rule files in the correct directory -filters: - # Match any .mdc files - - type: file_extension - pattern: "\\.mdc$" - # Match files that look like Cursor rules - - type: content - pattern: "(?s).*?" - # Match file creation events - - type: event - pattern: "file_create" - -actions: - - type: reject - conditions: - - pattern: "^(?!\\.\\/\\.cursor\\/rules\\/.*\\.mdc$)" - message: "Cursor rule files (.mdc) must be placed in the .cursor/rules directory" - - - type: suggest - message: | - When creating Cursor rules: - - 1. Always place rule files in PROJECT_ROOT/.cursor/rules/: - ``` - .cursor/rules/ - โ”œโ”€โ”€ your-rule-name.mdc - โ”œโ”€โ”€ another-rule.mdc - โ””โ”€โ”€ ... - ``` - - 2. Follow the naming convention: - - Use kebab-case for filenames - - Always use .mdc extension - - Make names descriptive of the rule's purpose - - 3. Directory structure: - ``` - PROJECT_ROOT/ - โ”œโ”€โ”€ .cursor/ - โ”‚ โ””โ”€โ”€ rules/ - โ”‚ โ”œโ”€โ”€ your-rule-name.mdc - โ”‚ โ””โ”€โ”€ ... - โ””โ”€โ”€ ... - ``` - - 4. Never place rule files: - - In the project root - - In subdirectories outside .cursor/rules - - In any other location - - Inside of the cursor-rules.mdc file - -examples: - - input: | - # Bad: Rule file in wrong location - rules/my-rule.mdc - my-rule.mdc - .rules/my-rule.mdc - - # Good: Rule file in correct location - .cursor/rules/my-rule.mdc - output: "Correctly placed Cursor rule file" - -metadata: - priority: high - version: 1.0 - \ No newline at end of file diff --git a/packages/cli-old/template/extras/_cursor/rules/filemaker-api.mdc b/packages/cli-old/template/extras/_cursor/rules/filemaker-api.mdc deleted file mode 100644 index dd6d6716..00000000 --- a/packages/cli-old/template/extras/_cursor/rules/filemaker-api.mdc +++ /dev/null @@ -1,176 +0,0 @@ ---- -description: | - This rule provides guidance for working with the FileMaker Data API in this project. It should be included when: - 1. Working with database operations or data fetching - 2. Encountering database-related errors or type issues - 3. Making changes to FileMaker schemas or layouts - 4. Implementing new data access patterns - 5. Discussing alternative data storage solutions - 6. Working with server-side API routes or actions -globs: - - "src/**/*.ts" - - "src/**/*.tsx" - - "**/fmschema.config.mjs" - - "src/**/actions/*.ts" -alwaysApply: true ---- -# FileMaker Data API Integration - -This rule documents how the FileMaker Data API is integrated and used in the project. - - -name: filemaker_api -description: Documents FileMaker Data API integration patterns and conventions. FileMaker is the ONLY data source for this application - no SQL or other databases should be used. -filters: - - type: file_extension - pattern: "\\.(ts|tsx)$" - - type: directory - pattern: "src/server/" - - type: content - pattern: "(@proofkit/cli|ZodError|typegen)" - -data_source_policy: - exclusive_source: "FileMaker Data API" - prohibited: - - "SQL databases" - - "NoSQL databases" - - "Local storage for persistent data" - - "Direct file system storage" - reason: "All data operations must go through FileMaker to maintain data integrity and business logic" - -troubleshooting: - priority_order: - - "ALWAYS run `{package-manager} typegen` first for ANY data loading issues" - - "DO NOT check environment variables unless you have a specific error message pointing to them" - - "Check for FileMaker schema changes" - - "Verify type definitions match current schema" - - "Review Zod validation errors" - rationale: "Most data loading issues are resolved by running typegen. Environment variables are rarely the cause of data loading problems and should not be investigated unless specific error messages indicate an authentication or connection issue." - -conventions: - api_setup: - - Uses @proofkit/fmdapi package version ^5.0.0 - - Configuration in fmschema.config.mjs - - Environment variables in .env for connection details - - Type generation via `{package-manager} typegen` command - - data_access: - - ALL data operations MUST use FileMaker Data API - - Server-side only API calls via @proofkit/fmdapi - - Type-safe database operations - - Centralized error handling - - Connection pooling and session management - - No direct database connections outside FileMaker - - data_operations: - create: - - Use layout.create({ fieldData: {...} }) - - Validate input against Zod schemas - - Returns recordId of created record - - Handle duplicates via FileMaker business logic - read: - - Use layout.get({ recordId }) for single record by ID - - Use layout.find({ query, limit, offset, sort }) for multiple records - - Use layout.maybeFindFirst({ query }) for optional single record - - Support for complex queries and sorting - update: - - Use layout.update({ recordId, fieldData }) - - Follow FileMaker field naming conventions - - Respect FileMaker validation rules - delete: - - Use layout.delete({ recordId }) - - Respect FileMaker deletion rules - - Handle cascading deletes via FileMaker - query_options: - - Limit and offset for pagination - - Sort by multiple fields with ascend/descend - - Complex query criteria with operators (==, *, etc.) - - Optional type-safe responses with Zod validation - - security: - - Credentials stored in environment variables - - No direct client-side FM API access - - API routes validate authentication - - Data sanitization before queries - - All database access through FileMaker only - -type_generation: - process: - - "IMPORTANT: Running `{package-manager} typegen` solves almost all data loading problems" - - "Run `{package-manager} typegen` after any FileMaker schema changes" - - "Run `{package-manager} typegen` as first step when troubleshooting data issues" - - "Types are generated from FileMaker database schema" - - "Generated types are used in server actions and components" - - "Zod schemas validate runtime data against types" - - common_issues: - schema_changes: - symptoms: - - "No data appearing in tables" - - "ZodError during runtime" - - "Missing or renamed fields" - - "Type mismatches in responses" - - "Empty query results" - solution: "ALWAYS run `{package-manager} typegen && {package-manager} tsc` first" - important_note: "Do NOT check environment variables as a cause for data loading problems unless you have a specific known error that points to environment variables. Most data loading issues are resolved by running typegen." - - field_types: - symptoms: - - "Unexpected null values" - - "Type conversion errors" - - "Invalid date formats" - solution: "Update Zod schemas and type definitions" - - security_notes: - - "Never display, log, or commit environment variables" - - "Never check environment variable values directly" - - "Keep .env files out of version control" - - "When troubleshooting, only verify if variables exist, never their values" - -patterns: - - Server actions wrap FM API calls - - Type definitions generated from FM schema - - Error boundaries for FM API errors - - Rate limiting on API routes - - Caching strategies for frequent queries - -dependencies: - fmdapi: "@proofkit/fmdapi@^5.0.0" - proofkit: "@proofkit/cli@^1.0.0" - -keywords: - database: - - "FileMaker" - - "FMREST" - - "Database schema" - - "Field types" - - "Type generation" - - "Schema changes" - - "Exclusive data source" - - "No SQL" - - "FileMaker only" - - "Data API" - errors: - - "ZodError" - - "TypeError" - - "ValidationError" - - "Missing field" - - "Runtime error" - commands: - - "typegen" - - "tsc" - - "type checking" - - "schema update" - operations: - - "FM.create" - - "FM.find" - - "FM.get" - - "FM.update" - - "FM.delete" - - "FileMaker layout" - - "FileMaker query" - -metadata: - priority: high - version: 1.0 - \ No newline at end of file diff --git a/packages/cli-old/template/extras/_cursor/rules/troubleshooting-patterns.mdc b/packages/cli-old/template/extras/_cursor/rules/troubleshooting-patterns.mdc deleted file mode 100644 index 797fd3cc..00000000 --- a/packages/cli-old/template/extras/_cursor/rules/troubleshooting-patterns.mdc +++ /dev/null @@ -1,240 +0,0 @@ ---- -description: | -globs: -alwaysApply: false ---- -# Troubleshooting and Maintenance Patterns - -This rule documents common issues, error patterns, and their solutions in the project. - - -name: troubleshooting_patterns -description: Documents common runtime errors, type errors, and solutions. All data operations MUST use FileMaker Data API exclusively. -filters: - - type: file_extension - pattern: "\\.(ts|tsx)$" - - type: content - pattern: "(Error|error|ZodError|TypeError|ValidationError|@proofkit/fmdapi)" - -initial_debugging_steps: - priority: "ALWAYS run `{package-manager} typegen` first for any data-related issues" - steps: - - "Run `{package-manager} typegen` to ensure types match FileMaker schema" - - "Check if error persists after typegen" - - "If error persists, check console for exact error messages" - - "Look for patterns in the troubleshooting guide below" - common_console_errors: - zod_errors: - pattern: "ZodError: [path] invalid_type..." - likely_cause: "Field name mismatch or missing field" - example: "ZodError: nameFirst expected string, got undefined" - solution: "Run typegen first, then check field names in FileMaker schema" - type_errors: - pattern: "TypeError: Cannot read property 'X' of undefined" - likely_cause: "Accessing field before data is loaded or field name mismatch" - solution: "Run typegen first, then add null checks or loading states" - network_errors: - pattern: "Failed to fetch" or "Network error" - likely_cause: "FileMaker connection issues" - solution: "Run typegen first, then check FileMaker server status and credentials" - -data_source_validation: - requirement: "All data operations must use FileMaker Data API exclusively" - first_step_for_data_issues: "ALWAYS run `{package-manager} typegen` first" - common_mistakes: - - "Attempting to use SQL queries" - - "Adding direct database connections" - - "Using local storage for persistent data" - - "Implementing alternative data stores" - - "Skipping typegen after FileMaker schema changes" - - "Using incorrect field names from old schema" - correct_approach: - - "Run typegen first" - - "Use @proofkit/fmdapi for all data operations" - - "Follow FileMaker layout and field conventions" - - "Use layout.create, layout.find, layout.get, layout.update, layout.delete" - - "Use layout.maybeFindFirst for optional records" - -error_patterns: - field_name_mismatches: - symptoms: - - "ZodError: invalid_type at path [fieldName]" - - "Property 'X' does not exist on type 'Y'" - - "TypeScript errors about missing properties" - common_examples: - - "nameFirst vs firstName" - - "lastName vs nameLast" - - "postalCode vs postal_code" - - "phoneNumber vs phone" - cause: "Mismatch between component field names and FileMaker schema" - solution: - steps: - - "Run `{package-manager} typegen` to update types" - - "Look at generated types in src/config/schemas/filemaker/" - - "Update component field names to match schema" - - "Check console for exact field name in error" - files_to_check: - - "src/config/schemas/filemaker/*.ts" - - "Component files using the fields" - - zod_validation_errors: - symptoms: - - "Runtime ZodError: invalid_type" - - "Zod schema validation failed" - - "Property not found in schema" - - "Unexpected field in response" - cause: "FileMaker database schema changes not reflected in TypeScript types" - solution: - steps: - - "Run `{package-manager} typegen` to regenerate types from FileMaker schema" - - "Run `{package-manager} tsc` to identify type mismatches" - - "Check console for exact error message" - - "Update affected components and server actions" - commands: - - "{package-manager} typegen" - - "{package-manager} tsc" - files_to_check: - - "src/server/actions/*" - - "src/server/schema/*" - - "fmschema.config.mjs" - - filemaker_connection: - symptoms: - - "ETIMEDOUT connecting to FileMaker" - - "Invalid FileMaker credentials" - - "Session token expired" - - "Layout not found" - - "Field not found in layout" - - "Invalid find criteria" - - "No data appearing or queries returning empty" - cause: "FileMaker connection, authentication, or query issues" - solution: - steps: - - "Run `{package-manager} typegen` to ensure schema is up to date" - - "Check FileMaker Server status" - - "Validate credentials and permissions" - - "Note: As an AI, you cannot directly check environment variables - always ask the user to verify them if this is determined to be the issue" - - "Verify layout names and field access" - - "Check FileMaker query syntax" - files_to_check: - - "src/server/lib/fm.ts" - - "fmschema.config.mjs" - - data_access_errors: - symptoms: - - "Invalid operation on FileMaker record" - - "Record not found" - - "Insufficient permissions" - - "Invalid find request" - cause: "Incorrect FileMaker Data API usage or permissions" - solution: - steps: - - "Run `{package-manager} typegen` to ensure schema is up to date" - - "Verify FileMaker layout privileges" - - "Check record existence before operations" - - "Validate find criteria format" - - "Use proper FM API methods" - files_to_check: - - "src/server/actions/*" - - "src/server/lib/fm.ts" - - type_errors: - symptoms: - - "Type ... is not assignable to type ..." - - "Property ... does not exist on type ..." - - "Argument of type ... is not assignable" - cause: "Mismatch between FileMaker schema and TypeScript types" - solution: - steps: - - "Run `{package-manager} typegen` to regenerate types" - - "Run `{package-manager} tsc` to identify type mismatches" - - "Update type definitions if needed" - - "Check for null/undefined handling" - commands: - - "{package-manager} typegen && {package-manager} tsc" - - data_sync_issues: - symptoms: - - "Missing fields in table" - - "Unexpected null values" - - "Fields showing as blank" - - "Type mismatches between FM and frontend" - first_step: "ALWAYS run `{package-manager} typegen` first" - cause: "Mismatch between FileMaker schema and TypeScript types, or outdated type definitions" - solution: - steps: - - "Run `{package-manager} typegen` to regenerate types from FileMaker schema" - - "Check for any type errors in the console" - - "Verify field names match exactly between FM and generated types" - - "Update components if field names have changed" - commands: - - "{package-manager} typegen" - - "{package-manager} tsc" - files_to_check: - - "src/config/schemas/filemaker/*.ts" - - "fmschema.config.mjs" - -maintenance_tasks: - schema_sync: - description: "Keep FileMaker schema and TypeScript types in sync" - frequency: "After any FileMaker schema changes" - steps: - - "Run typegen to update types" - - "Run TypeScript compiler" - - "Update affected components" - impact: "Prevents runtime errors and type mismatches" - - type_checking: - description: "Regular type checking for early error detection" - frequency: "Before deployments and after schema changes" - commands: - - "{package-manager} tsc --noEmit" - impact: "Catches type errors before runtime" - -keywords: - errors: - - "ZodError" - - "TypeError" - - "ValidationError" - - "Schema mismatch" - - "Type mismatch" - - "Runtime error" - - "Database schema" - - "Type generation" - - "FileMaker fields" - - "Missing property" - - "Invalid type" - - "Layout not found" - - "Field not found" - - "Invalid find request" - solutions: - - "typegen" - - "tsc" - - "type checking" - - "schema update" - - "validation fix" - - "error handling" - - "FM API methods" - - "FileMaker layout" - operations: - - "layout.create" - - "layout.find" - - "layout.get" - - "layout.update" - - "layout.delete" - - "layout.maybeFindFirst" - - "recordId" - - "fieldData" - - "query parameters" - - "sort options" - data_source: - - "FileMaker only" - - "No SQL" - - "FM Data API" - - "Exclusive data source" - - "@proofkit/fmdapi" - -metadata: - priority: high - version: 1.0 - \ No newline at end of file diff --git a/packages/cli-old/template/extras/_cursor/rules/ui-components.mdc b/packages/cli-old/template/extras/_cursor/rules/ui-components.mdc deleted file mode 100644 index 78ec63ad..00000000 --- a/packages/cli-old/template/extras/_cursor/rules/ui-components.mdc +++ /dev/null @@ -1,57 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# UI Components and Styling - -This rule documents the UI component library and styling conventions used in the project. - - -name: ui_components -description: Documents UI component library usage and styling conventions -filters: - - type: file_extension - pattern: "\\.(ts|tsx)$" - - type: directory - pattern: "src/components/" - - type: content - pattern: "@mantine/" - -conventions: - component_library: - - Mantine v7 as primary UI framework - - Tabler icons for iconography - - Mantine React Table for data grids - - Custom components extend Mantine base - - styling: - - PostCSS for processing - - Mantine theme customization - - CSS modules for component styles - - CSS variables for theming - - components: - - Atomic design principles - - Consistent prop interfaces - - Accessibility first - - Responsive design patterns - - forms: - - React Hook Form for form state - - Zod for validation schemas - - Mantine form components - - Custom form layouts - -dependencies: - mantine_core: "^7.17.0" - mantine_hooks: "^7.17.0" - mantine_dates: "^7.17.0" - mantine_notifications: "^7.17.0" - react_hook_form: "^7.54.2" - zod: "^3.24.2" - -metadata: - priority: high - version: 1.0 - \ No newline at end of file diff --git a/packages/cli-old/template/extras/config/drizzle-config-mysql.ts b/packages/cli-old/template/extras/config/drizzle-config-mysql.ts deleted file mode 100644 index 1f71d754..00000000 --- a/packages/cli-old/template/extras/config/drizzle-config-mysql.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type Config } from "drizzle-kit"; - -import { env } from "~/env"; - -export default { - schema: "./src/server/db/schema.ts", - dialect: "mysql", - dbCredentials: { - url: env.DATABASE_URL, - }, - tablesFilter: ["project1_*"], -} satisfies Config; diff --git a/packages/cli-old/template/extras/config/drizzle-config-postgres.ts b/packages/cli-old/template/extras/config/drizzle-config-postgres.ts deleted file mode 100644 index d2a21ed7..00000000 --- a/packages/cli-old/template/extras/config/drizzle-config-postgres.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type Config } from "drizzle-kit"; - -import { env } from "~/env"; - -export default { - schema: "./src/server/db/schema.ts", - dialect: "postgresql", - dbCredentials: { - url: env.DATABASE_URL, - }, - tablesFilter: ["project1_*"], -} satisfies Config; diff --git a/packages/cli-old/template/extras/config/drizzle-config-sqlite.ts b/packages/cli-old/template/extras/config/drizzle-config-sqlite.ts deleted file mode 100644 index 34f8fa24..00000000 --- a/packages/cli-old/template/extras/config/drizzle-config-sqlite.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type Config } from "drizzle-kit"; - -import { env } from "~/env"; - -export default { - schema: "./src/server/db/schema.ts", - dialect: "sqlite", - dbCredentials: { - url: env.DATABASE_URL, - }, - tablesFilter: ["project1_*"], -} satisfies Config; diff --git a/packages/cli-old/template/extras/config/fmschema.config.mjs b/packages/cli-old/template/extras/config/fmschema.config.mjs deleted file mode 100644 index 660edd23..00000000 --- a/packages/cli-old/template/extras/config/fmschema.config.mjs +++ /dev/null @@ -1,9 +0,0 @@ -/** @type {import("@proofkit/fmdapi/dist/utils/typegen/types.d.ts").GenerateSchemaOptions} */ -export const config = { - clientSuffix: "Layout", - schemas: [ - // add your layouts and name schemas here - ], - clearOldFiles: true, - path: "./src/config/schemas/filemaker", -}; diff --git a/packages/cli-old/template/extras/config/get-query-client.ts b/packages/cli-old/template/extras/config/get-query-client.ts deleted file mode 100644 index 44598cba..00000000 --- a/packages/cli-old/template/extras/config/get-query-client.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { QueryClient } from "@tanstack/react-query"; -import { cache } from "react"; - -// cache() is scoped per request, so we don't leak data between requests -const getQueryClient = cache(() => new QueryClient()); -export default getQueryClient; diff --git a/packages/cli-old/template/extras/config/postcss.config.cjs b/packages/cli-old/template/extras/config/postcss.config.cjs deleted file mode 100644 index 4cdb2f43..00000000 --- a/packages/cli-old/template/extras/config/postcss.config.cjs +++ /dev/null @@ -1,7 +0,0 @@ -const config = { - plugins: { - tailwindcss: {}, - }, -}; - -module.exports = config; diff --git a/packages/cli-old/template/extras/config/query-provider-vite.tsx b/packages/cli-old/template/extras/config/query-provider-vite.tsx deleted file mode 100644 index 5af4ad27..00000000 --- a/packages/cli-old/template/extras/config/query-provider-vite.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; - -const queryClient = new QueryClient(); - -export default function QueryProvider({ - children, -}: { - children: React.ReactNode; -}) { - return ( - - {children} - - - ); -} diff --git a/packages/cli-old/template/extras/config/query-provider.tsx b/packages/cli-old/template/extras/config/query-provider.tsx deleted file mode 100644 index 2afa87bd..00000000 --- a/packages/cli-old/template/extras/config/query-provider.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import { QueryClientProvider } from "@tanstack/react-query"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; - -import getQueryClient from "./get-query-client"; - -export default function QueryProvider({ - children, -}: { - children: React.ReactNode; -}) { - const queryClient = getQueryClient(); - - return ( - - {children} - - - ); -} diff --git a/packages/cli-old/template/extras/emailProviders/none/email.tsx b/packages/cli-old/template/extras/emailProviders/none/email.tsx deleted file mode 100644 index b8ed06e6..00000000 --- a/packages/cli-old/template/extras/emailProviders/none/email.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { AuthCodeEmail } from "@/emails/auth-code"; -import { render } from "@react-email/render"; - -export async function sendEmail({ - to, - code, - type, -}: { - to: string; - code: string; - type: "verification" | "password-reset"; -}) { - // this is the HTML body of the email to be send - const body = await render( - - ); - const subject = - type === "verification" ? "Verify Your Email" : "Reset Your Password"; - - // TODO: Customize this function to actually send the email to your users - // Learn more: https://proofkit.proof.sh/auth/fm-addon - console.warn("TODO: Customize this function to actually send to your users"); - console.log(`To ${to}: Your ${type} code is ${code}`); -} diff --git a/packages/cli-old/template/extras/emailProviders/plunk/email.tsx b/packages/cli-old/template/extras/emailProviders/plunk/email.tsx deleted file mode 100644 index ef94053e..00000000 --- a/packages/cli-old/template/extras/emailProviders/plunk/email.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { AuthCodeEmail } from "@/emails/auth-code"; -import { render } from "@react-email/render"; - -import { plunk } from "../services/plunk"; - -export async function sendEmail({ - to, - code, - type, -}: { - to: string; - code: string; - type: "verification" | "password-reset"; -}) { - // this is the HTML body of the email to be send - const body = await render( - - ); - const subject = - type === "verification" ? "Verify Your Email" : "Reset Your Password"; - - await plunk.emails.send({ - to, - subject, - body, - }); -} diff --git a/packages/cli-old/template/extras/emailProviders/plunk/service.ts b/packages/cli-old/template/extras/emailProviders/plunk/service.ts deleted file mode 100644 index 9f6a3ca6..00000000 --- a/packages/cli-old/template/extras/emailProviders/plunk/service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { env } from "@/config/env"; -import Plunk from "@plunk/node"; - -export const plunk = new Plunk(env.PLUNK_API_KEY); diff --git a/packages/cli-old/template/extras/emailProviders/resend/email.tsx b/packages/cli-old/template/extras/emailProviders/resend/email.tsx deleted file mode 100644 index 5ca905b8..00000000 --- a/packages/cli-old/template/extras/emailProviders/resend/email.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { AuthCodeEmail } from "@/emails/auth-code"; - -import { resend } from "../services/resend"; - -export async function sendEmail({ - to, - code, - type, -}: { - to: string; - code: string; - type: "verification" | "password-reset"; -}) { - const subject = - type === "verification" ? "Verify Your Email" : "Reset Your Password"; - - await resend.emails.send({ - // TODO: Change this to our own email after verifying your domain with Resend - from: "ProofKit ", - to, - subject, - react: , - }); -} diff --git a/packages/cli-old/template/extras/emailProviders/resend/service.ts b/packages/cli-old/template/extras/emailProviders/resend/service.ts deleted file mode 100644 index 9af08cd1..00000000 --- a/packages/cli-old/template/extras/emailProviders/resend/service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { env } from "@/config/env"; -import { Resend } from "resend"; - -export const resend = new Resend(env.RESEND_API_KEY); diff --git a/packages/cli-old/template/extras/emailTemplates/auth-code.tsx b/packages/cli-old/template/extras/emailTemplates/auth-code.tsx deleted file mode 100644 index 3661a457..00000000 --- a/packages/cli-old/template/extras/emailTemplates/auth-code.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { Body, Container, Head, Heading, Html, Img, Section, Text } from "@react-email/components"; - -interface AuthCodeEmailProps { - validationCode: string; - type: "verification" | "password-reset"; -} - -export const AuthCodeEmail = ({ validationCode, type }: AuthCodeEmailProps) => ( - - - - - ProofKit - {type === "verification" ? "Verify Your Email" : "Reset Your Password"} - - Enter the following code to {type === "verification" ? "verify your email" : "reset your password"} - -
- {validationCode} -
- If you did not request this code, you can ignore this email. -
- - -); - -AuthCodeEmail.PreviewProps = { - validationCode: "D7CU4GOV", - type: "verification", -} as AuthCodeEmailProps; - -export default AuthCodeEmail; - -const main = { - backgroundColor: "#ffffff", - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", -}; - -const container = { - backgroundColor: "#ffffff", - border: "1px solid #eee", - borderRadius: "5px", - boxShadow: "0 5px 10px rgba(20,50,70,.2)", - marginTop: "20px", - maxWidth: "360px", - margin: "0 auto", - padding: "68px 0 130px", -}; - -const logo: React.CSSProperties = { - margin: "0 auto", -}; - -const tertiary = { - color: "#0a85ea", - fontSize: "11px", - fontWeight: 700, - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", - height: "16px", - letterSpacing: "0", - lineHeight: "16px", - margin: "16px 8px 8px 8px", - textTransform: "uppercase" as const, - textAlign: "center" as const, -}; - -const secondary = { - color: "#000", - display: "inline-block", - fontFamily: "HelveticaNeue-Medium,Helvetica,Arial,sans-serif", - fontSize: "20px", - fontWeight: 500, - lineHeight: "24px", - marginBottom: "0", - marginTop: "0", - textAlign: "center" as const, - padding: "0 40px", -}; - -const codeContainer = { - background: "rgba(0,0,0,.05)", - borderRadius: "4px", - margin: "16px auto 14px", - verticalAlign: "middle", - width: "280px", -}; - -const code = { - color: "#000", - display: "inline-block", - fontFamily: "HelveticaNeue-Bold", - fontSize: "32px", - fontWeight: 700, - letterSpacing: "6px", - lineHeight: "40px", - paddingBottom: "8px", - paddingTop: "8px", - margin: "0 auto", - width: "100%", - textAlign: "center" as const, -}; - -const paragraph = { - color: "#444", - fontSize: "15px", - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", - letterSpacing: "0", - lineHeight: "23px", - padding: "0 40px", - margin: "0", - textAlign: "center" as const, -}; - -const link = { - color: "#444", - textDecoration: "underline", -}; - -const footer = { - color: "#000", - fontSize: "12px", - fontWeight: 800, - letterSpacing: "0", - lineHeight: "23px", - margin: "0", - marginTop: "20px", - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", - textAlign: "center" as const, - textTransform: "uppercase" as const, -}; diff --git a/packages/cli-old/template/extras/emailTemplates/generic.tsx b/packages/cli-old/template/extras/emailTemplates/generic.tsx deleted file mode 100644 index f4b34ba2..00000000 --- a/packages/cli-old/template/extras/emailTemplates/generic.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Body, Button, Container, Head, Heading, Hr, Html, Img, Section, Text } from "@react-email/components"; - -export interface GenericEmailProps { - title?: string; - description?: string; - ctaText?: string; - ctaHref?: string; - footer?: string; -} - -export const GenericEmail = ({ title, description, ctaText, ctaHref, footer }: GenericEmailProps) => ( - - - - - ProofKit - - {title ? {title} : null} - - {description ? {description} : null} - - {ctaText && ctaHref ? ( -
- -
- ) : null} - - {(title || description || (ctaText && ctaHref)) &&
} - - {footer ? {footer} : null} -
- - -); - -GenericEmail.PreviewProps = { - title: "Welcome to ProofKit", - description: "Thanks for trying ProofKit. This is a sample email template you can customize.", - ctaText: "Get Started", - ctaHref: "https://proofkit.proof.sh", - footer: "You received this email because you signed up for updates.", -} as GenericEmailProps; - -export default GenericEmail; - -const styles = { - main: { - backgroundColor: "#ffffff", - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", - }, - container: { - backgroundColor: "#ffffff", - border: "1px solid #eee", - borderRadius: "5px", - boxShadow: "0 5px 10px rgba(20,50,70,.2)", - marginTop: "20px", - maxWidth: "520px", - margin: "0 auto", - padding: "48px 32px 36px", - } as React.CSSProperties, - logo: { - margin: "0 auto 12px", - display: "block", - } as React.CSSProperties, - title: { - color: "#111827", - fontSize: "22px", - fontWeight: 600, - lineHeight: "28px", - margin: "8px 0 4px", - textAlign: "center" as const, - }, - description: { - color: "#374151", - fontSize: "15px", - lineHeight: "22px", - margin: "8px 0 0", - textAlign: "center" as const, - }, - ctaSection: { - textAlign: "center" as const, - marginTop: "20px", - }, - ctaButton: { - backgroundColor: "#0a85ea", - color: "#fff", - fontSize: "14px", - fontWeight: 600, - lineHeight: "20px", - textDecoration: "none", - display: "inline-block", - padding: "10px 16px", - borderRadius: "6px", - } as React.CSSProperties, - hr: { - borderColor: "#e5e7eb", - margin: "24px 0 12px", - }, - footer: { - color: "#6b7280", - fontSize: "12px", - lineHeight: "18px", - textAlign: "center" as const, - }, -}; diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts deleted file mode 100644 index 49191dfa..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts +++ /dev/null @@ -1,97 +0,0 @@ -"use server"; - -import { - createEmailVerificationRequest, - sendVerificationEmail, - setEmailVerificationRequestCookie, -} from "@/server/auth/utils/email-verification"; -import { - verifyPasswordHash, - verifyPasswordStrength, -} from "@/server/auth/utils/password"; -import { - createSession, - generateSessionToken, - getCurrentSession, - invalidateUserSessions, - setSessionTokenCookie, -} from "@/server/auth/utils/session"; -import { - checkEmailAvailability, - updateUserPassword, - validateLogin, -} from "@/server/auth/utils/user"; -import { actionClient } from "@/server/safe-action"; -import { redirect } from "next/navigation"; - -import { updateEmailSchema, updatePasswordSchema } from "./schema"; - -export const updateEmailAction = actionClient - .schema(updateEmailSchema) - .action(async ({ parsedInput }) => { - const { session, user } = await getCurrentSession(); - if (session === null) { - return { - message: "Not authenticated", - }; - } - - const { email } = parsedInput; - - const emailAvailable = await checkEmailAvailability(email); - if (!emailAvailable) { - return { - error: "This email is already used", - }; - } - - const verificationRequest = await createEmailVerificationRequest( - user.id, - email - ); - await sendVerificationEmail( - verificationRequest.email, - verificationRequest.code - ); - await setEmailVerificationRequestCookie(verificationRequest); - return redirect("/auth/verify-email"); - }); - -export const updatePasswordAction = actionClient - .schema(updatePasswordSchema) - .action(async ({ parsedInput }) => { - const { confirmNewPassword, currentPassword, newPassword } = parsedInput; - - const { session, user } = await getCurrentSession(); - if (session === null) { - return { - error: "Not authenticated", - }; - } - - const strongPassword = await verifyPasswordStrength(newPassword); - if (!strongPassword) { - return { - error: "Weak password", - }; - } - - const validPassword = Boolean( - await validateLogin(user.email, currentPassword) - ); - if (!validPassword) { - return { - error: "Incorrect password", - }; - } - - await invalidateUserSessions(user.id); - await updateUserPassword(user.id, newPassword); - - const sessionToken = generateSessionToken(); - const newSession = await createSession(sessionToken, user.id); - await setSessionTokenCookie(sessionToken, newSession.expiresAt); - return { - message: "Password updated", - }; - }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx deleted file mode 100644 index 76431716..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { getCurrentSession } from "@/server/auth/utils/session"; -import { Anchor, Container, Paper, Stack, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; - -import UpdateEmailForm from "./profile-form"; -import UpdatePasswordForm from "./reset-password-form"; - -// import EmailVerificationForm from "./email-verification-form"; - -export default async function Page() { - const { user } = await getCurrentSession(); - - if (user === null) { - return redirect("/auth/login"); - } - - return ( - - Profile Details - - - - - - - - - ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx deleted file mode 100644 index 13e3853a..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { - Anchor, - Button, - Group, - Paper, - PasswordInput, - Stack, - Text, - TextInput, -} from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; - -import { updateEmailAction } from "./actions"; -import { updateEmailSchema } from "./schema"; - -export default function UpdateEmailForm({ - currentEmail, -}: { - currentEmail: string; -}) { - const { form, handleSubmitWithAction, action } = useHookFormAction( - updateEmailAction, - zodResolver(updateEmailSchema), - { formProps: { defaultValues: { email: currentEmail } } } - ); - - return ( -
- - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - {form.formState.isDirty && ( - - - - )} - -
- ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx deleted file mode 100644 index b22bee20..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx +++ /dev/null @@ -1,112 +0,0 @@ -"use client"; - -import { showSuccessNotification } from "@/utils/notification-helpers"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { - Anchor, - Button, - Group, - Paper, - PasswordInput, - Stack, - Text, - TextInput, -} from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; -import { useState } from "react"; - -import { updatePasswordAction } from "./actions"; -import { updatePasswordSchema } from "./schema"; - -export default function UpdatePasswordForm() { - const [showForm, setShowForm] = useState(false); - const { form, handleSubmitWithAction, action } = useHookFormAction( - updatePasswordAction, - zodResolver(updatePasswordSchema), - { - formProps: { defaultValues: {} }, - actionProps: { - onSuccess: ({ data }) => { - if (data?.message) { - showSuccessNotification(data.message); - setShowForm(false); - } - }, - }, - } - ); - - if (!showForm) { - return ( - - - - - ); - } - - return ( -
- - - - - - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - - - -
- ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts deleted file mode 100644 index 046783e4..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { z } from "zod/v4"; - -export const updateEmailSchema = z.object({ - email: z.string().email(), -}); - -export const updatePasswordSchema = z - .object({ - currentPassword: z.string(), - newPassword: z - .string() - .min(8, { message: "Password must be at least 8 characters long" }) - .max(255, { message: "Password is too long" }), - confirmNewPassword: z.string(), - }) - .refine((data) => data.newPassword === data.confirmNewPassword, { - path: ["confirmNewPassword"], - message: "Passwords do not match", - }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts deleted file mode 100644 index 78c14d96..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts +++ /dev/null @@ -1,39 +0,0 @@ -"use server"; - -import { - createPasswordResetSession, - invalidateUserPasswordResetSessions, - sendPasswordResetEmail, - setPasswordResetSessionTokenCookie, -} from "@/server/auth/utils/password-reset"; -import { generateSessionToken } from "@/server/auth/utils/session"; -import { getUserFromEmail } from "@/server/auth/utils/user"; -import { actionClient } from "@/server/safe-action"; -import { redirect } from "next/navigation"; - -import { forgotPasswordSchema } from "./schema"; - -export const forgotPasswordAction = actionClient - .schema(forgotPasswordSchema) - .action(async ({ parsedInput }) => { - const { email } = parsedInput; - - const user = await getUserFromEmail(email); - if (user === null) { - return { - error: "Account does not exist", - }; - } - - await invalidateUserPasswordResetSessions(user.id); - const sessionToken = generateSessionToken(); - const session = await createPasswordResetSession( - sessionToken, - user.id, - user.email - ); - - await sendPasswordResetEmail(session.email, session.code); - await setPasswordResetSessionTokenCookie(sessionToken, session.expires_at); - return redirect("/auth/reset-password/verify-email"); - }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx deleted file mode 100644 index 695d7f2c..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { Button, Paper, Stack, Text, TextInput } from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; - -import { forgotPasswordAction } from "./actions"; -import { forgotPasswordSchema } from "./schema"; - -export default function ForgotForm() { - const { form, handleSubmitWithAction, action } = useHookFormAction( - forgotPasswordAction, - zodResolver(forgotPasswordSchema), - {} - ); - - return ( -
- - - - - {action.result.data?.error && ( - {action.result.data.error} - )} - - - - -
- ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx deleted file mode 100644 index 09be86ba..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Anchor, Container, Text, Title } from "@mantine/core"; - -import ForgotForm from "./forgot-form"; - -export default async function Page() { - return ( - - Forgot Password - - Enter your email for a link to reset your password. - - - - - - - Back to login - - - - ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts deleted file mode 100644 index 15829b1a..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from "zod/v4"; - -export const forgotPasswordSchema = z.object({ - email: z.string().email(), -}); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/actions.ts deleted file mode 100644 index ca66a9df..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/actions.ts +++ /dev/null @@ -1,35 +0,0 @@ -"use server"; - -import { getRedirectCookie } from "@/server/auth/utils/redirect"; -import { - createSession, - generateSessionToken, - setSessionTokenCookie, -} from "@/server/auth/utils/session"; -import { validateLogin } from "@/server/auth/utils/user"; -import { actionClient } from "@/server/safe-action"; -import { redirect } from "next/navigation"; - -import { loginSchema } from "./schema"; - -export const loginAction = actionClient - .schema(loginSchema) - .action(async ({ parsedInput }) => { - const { email, password } = parsedInput; - const user = await validateLogin(email, password); - - if (user === null) { - return { error: "Invalid email or password" }; - } - - const sessionToken = generateSessionToken(); - const session = await createSession(sessionToken, user.id); - setSessionTokenCookie(sessionToken, session.expiresAt); - - if (!user.emailVerified) { - return redirect("/auth/verify-email"); - } - - const redirectTo = await getRedirectCookie(); - return redirect(redirectTo); - }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/login-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/login-form.tsx deleted file mode 100644 index c9967827..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/login-form.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { - Anchor, - Button, - Group, - Paper, - PasswordInput, - Stack, - Text, - TextInput, -} from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; - -import { loginAction } from "./actions"; -import { loginSchema } from "./schema"; - -export default function LoginForm() { - const { form, handleSubmitWithAction, action } = useHookFormAction( - loginAction, - zodResolver(loginSchema), - {} - ); - - return ( -
- - - - - - - - Forgot password? - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - -
- ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/page.tsx deleted file mode 100644 index a98eb6c3..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { getCurrentSession } from "@/server/auth/utils/session"; -import { Anchor, Container, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; - -import LoginForm from "./login-form"; - -export default async function Page() { - const { session } = await getCurrentSession(); - - if (session !== null) { - return redirect("/"); - } - - return ( - - Welcome back! - - Do not have an account yet?{" "} - - Create account - - - - - - ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/schema.ts deleted file mode 100644 index 66276d2f..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/login/schema.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { z } from "zod/v4"; - -export const loginSchema = z.object({ - email: z.string().email(), - password: z.string(), -}); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts deleted file mode 100644 index a781546c..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts +++ /dev/null @@ -1,53 +0,0 @@ -"use server"; - -import { verifyPasswordStrength } from "@/server/auth/utils/password"; -import { - deletePasswordResetSessionTokenCookie, - invalidateUserPasswordResetSessions, - validatePasswordResetSessionRequest, -} from "@/server/auth/utils/password-reset"; -import { - createSession, - generateSessionToken, - invalidateUserSessions, - setSessionTokenCookie, -} from "@/server/auth/utils/session"; -import { updateUserPassword } from "@/server/auth/utils/user"; -import { actionClient } from "@/server/safe-action"; -import { redirect } from "next/navigation"; - -import { resetPasswordSchema } from "./schema"; - -export const resetPasswordAction = actionClient - .schema(resetPasswordSchema) - .action(async ({ parsedInput }) => { - const { password } = parsedInput; - const { session: passwordResetSession, user } = - await validatePasswordResetSessionRequest(); - if (passwordResetSession === null) { - return { - error: "Not authenticated", - }; - } - if (!passwordResetSession.email_verified) { - return { - error: "Forbidden", - }; - } - - const strongPassword = await verifyPasswordStrength(password); - if (!strongPassword) { - return { - error: "Weak password", - }; - } - await invalidateUserPasswordResetSessions(passwordResetSession.id_user); - await invalidateUserSessions(passwordResetSession.id_user); - await updateUserPassword(passwordResetSession.id_user, password); - - const sessionToken = generateSessionToken(); - const session = await createSession(sessionToken, user.id); - await setSessionTokenCookie(sessionToken, session.expiresAt); - await deletePasswordResetSessionTokenCookie(); - return redirect("/"); - }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx deleted file mode 100644 index 9a164a1d..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { env } from "@/config/env"; -import { validatePasswordResetSessionRequest } from "@/server/auth/utils/password-reset"; -import { Alert, Anchor, Container, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; - -import ResetPasswordForm from "./reset-password-form"; - -export default async function Page() { - const { session, user } = await validatePasswordResetSessionRequest(); - if (session === null) { - return redirect("/auth/forgot-password"); - } - if (!session.email_verified) { - return redirect("/auth/reset-password/verify-email"); - } - - return ( - - Reset Password - - Enter your new password. - - - - - - - Back to login - - - - ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx deleted file mode 100644 index e11b3acd..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { - Button, - Paper, - PasswordInput, - Stack, - Text, - TextInput, -} from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; - -import { resetPasswordAction } from "./actions"; -import { resetPasswordSchema } from "./schema"; - -export default function ForgotForm() { - const { form, handleSubmitWithAction, action } = useHookFormAction( - resetPasswordAction, - zodResolver(resetPasswordSchema), - {} - ); - - return ( -
- - - - - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - - -
- ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts deleted file mode 100644 index 8315fd2c..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z } from "zod/v4"; - -export const resetPasswordSchema = z - .object({ - password: z - .string() - .min(8, { message: "Your password should be at least 8 characters" }) - .max(255, { message: "Password is too long" }), - confirmPassword: z.string(), - }) - .refine((data) => data.password === data.confirmPassword, { - path: ["confirmPassword"], - message: "Passwords do not match", - }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts deleted file mode 100644 index 4ce0b1b7..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts +++ /dev/null @@ -1,46 +0,0 @@ -"use server"; - -import { - setPasswordResetSessionAsEmailVerified, - validatePasswordResetSessionRequest, -} from "@/server/auth/utils/password-reset"; -import { setUserAsEmailVerifiedIfEmailMatches } from "@/server/auth/utils/user"; -import { actionClient } from "@/server/safe-action"; -import { redirect } from "next/navigation"; - -import { verifyEmailSchema } from "./schema"; - -export const verifyEmailAction = actionClient - .schema(verifyEmailSchema) - .action(async ({ parsedInput }) => { - const { session } = await validatePasswordResetSessionRequest(); - if (session === null) { - return { - error: "Not authenticated", - }; - } - if (Boolean(session.email_verified)) { - return { - error: "Forbidden", - }; - } - - const { code } = parsedInput; - - if (code !== session.code) { - return { - error: "Incorrect code", - }; - } - await setPasswordResetSessionAsEmailVerified(session.id); - const emailMatches = await setUserAsEmailVerifiedIfEmailMatches( - session.id_user, - session.email - ); - if (!emailMatches) { - return { - error: "Please restart the process", - }; - } - return redirect("/auth/reset-password"); - }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx deleted file mode 100644 index b3796b06..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { env } from "@/config/env"; -import { validatePasswordResetSessionRequest } from "@/server/auth/utils/password-reset"; -import { Alert, Anchor, Container, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; - -import VerifyEmailForm from "./verify-email-form"; - -export default async function Page() { - const { session } = await validatePasswordResetSessionRequest(); - if (session === null) { - return redirect("/auth/forgot-password"); - } - if (session.email_verified) { - return redirect("/auth/reset-password"); - } - - return ( - - Verify Email - - Enter the code sent to your email. - - - - - - - Back to login - - - - ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts deleted file mode 100644 index 37d5311a..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from "zod/v4"; - -export const verifyEmailSchema = z.object({ - code: z.string().length(8), -}); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx deleted file mode 100644 index 2d454b7e..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { Button, Paper, PinInput, Stack, Text } from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; - -import { verifyEmailAction } from "./actions"; -import { verifyEmailSchema } from "./schema"; - -export default function VerifyEmailForm() { - const { form, handleSubmitWithAction, action } = useHookFormAction( - verifyEmailAction, - zodResolver(verifyEmailSchema), - {} - ); - - return ( -
- - - { - form.setValue("code", value); - if (value.length === 8) { - handleSubmitWithAction(); - } - }} - error={!!form.formState.errors.code?.message} - autoFocus - /> - {form.formState.errors.code?.message && ( - {form.formState.errors.code.message} - )} - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - -
- ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/actions.ts deleted file mode 100644 index 3faa5d0f..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/actions.ts +++ /dev/null @@ -1,50 +0,0 @@ -"use server"; - -import { - createEmailVerificationRequest, - sendVerificationEmail, - setEmailVerificationRequestCookie, -} from "@/server/auth/utils/email-verification"; -import { verifyPasswordStrength } from "@/server/auth/utils/password"; -import { - createSession, - generateSessionToken, - setSessionTokenCookie, -} from "@/server/auth/utils/session"; -import { checkEmailAvailability, createUser } from "@/server/auth/utils/user"; -import { actionClient } from "@/server/safe-action"; -import { redirect } from "next/navigation"; - -import { signupSchema } from "./schema"; - -export const signupAction = actionClient - .schema(signupSchema) - .action(async ({ parsedInput }) => { - const { email, password } = parsedInput; - const emailAvailable = await checkEmailAvailability(email); - if (!emailAvailable) { - return { error: "Email already in use" }; - } - - const passwordStrong = await verifyPasswordStrength(password); - if (!passwordStrong) { - return { error: "Password is too weak" }; - } - - const user = await createUser(email, password); - const emailVerificationRequest = await createEmailVerificationRequest( - user.id, - user.email - ); - await sendVerificationEmail( - emailVerificationRequest.email, - emailVerificationRequest.code - ); - await setEmailVerificationRequestCookie(emailVerificationRequest); - - const sessionToken = generateSessionToken(); - const session = await createSession(sessionToken, user.id); - setSessionTokenCookie(sessionToken, session.expiresAt); - - return redirect("/auth/verify-email"); - }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/page.tsx deleted file mode 100644 index 056d5284..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { getCurrentSession } from "@/server/auth/utils/session"; -import { Anchor, Container, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; - -import SignupForm from "./signup-form"; - -export default async function Page() { - const { session } = await getCurrentSession(); - - if (session !== null) { - return redirect("/"); - } - - return ( - - Create account - - Already have an account?{" "} - - Sign in - - - - - - ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/schema.ts deleted file mode 100644 index e15638ca..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/schema.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { z } from "zod/v4"; - -export const signupSchema = z - .object({ - email: z.string().email(), - password: z.string().min(8), - confirmPassword: z.string(), - }) - .refine((data) => data.password === data.confirmPassword, { - path: ["confirmPassword"], - message: "Passwords do not match", - }); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx deleted file mode 100644 index 75c0e4fd..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { - Anchor, - Button, - Group, - Paper, - PasswordInput, - Stack, - Text, - TextInput, -} from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; - -import { signupAction } from "./actions"; -import { signupSchema } from "./schema"; - -export default function SignupForm() { - const { form, handleSubmitWithAction, action } = useHookFormAction( - signupAction, - zodResolver(signupSchema), - {} - ); - - return ( -
- - - - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - -
- ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts deleted file mode 100644 index 3ad9697a..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts +++ /dev/null @@ -1,109 +0,0 @@ -"use server"; - -import { - createEmailVerificationRequest, - deleteEmailVerificationRequestCookie, - deleteUserEmailVerificationRequest, - getUserEmailVerificationRequestFromRequest, - sendVerificationEmail, - setEmailVerificationRequestCookie, -} from "@/server/auth/utils/email-verification"; -import { invalidateUserPasswordResetSessions } from "@/server/auth/utils/password-reset"; -import { getRedirectCookie } from "@/server/auth/utils/redirect"; -import { getCurrentSession } from "@/server/auth/utils/session"; -import { updateUserEmailAndSetEmailAsVerified } from "@/server/auth/utils/user"; -import { actionClient } from "@/server/safe-action"; -import { redirect } from "next/navigation"; - -import { emailVerificationSchema } from "./schema"; - -export const verifyEmailAction = actionClient - .schema(emailVerificationSchema) - .action(async ({ parsedInput, ctx }) => { - const { session, user } = await getCurrentSession(); - if (session === null) { - return { - error: "Not authenticated", - }; - } - - let verificationRequest = - await getUserEmailVerificationRequestFromRequest(); - if (verificationRequest === null) { - return { - error: "Not authenticated", - }; - } - const { code } = parsedInput; - if (verificationRequest.expires_at === null) { - return { - error: "Verification code expired", - }; - } - - if (Date.now() >= verificationRequest.expires_at * 1000) { - verificationRequest = await createEmailVerificationRequest( - verificationRequest.id_user, - verificationRequest.email - ); - await sendVerificationEmail( - verificationRequest.email, - verificationRequest.code - ); - return { - error: - "The verification code was expired. We sent another code to your inbox.", - }; - } - if (verificationRequest.code !== code) { - return { - error: "Incorrect code.", - }; - } - await deleteUserEmailVerificationRequest(user.id); - await invalidateUserPasswordResetSessions(user.id); - await updateUserEmailAndSetEmailAsVerified( - user.id, - verificationRequest.email - ); - await deleteEmailVerificationRequestCookie(); - - const redirectTo = await getRedirectCookie(); - return redirect(redirectTo); - }); - -export const resendEmailVerificationAction = actionClient.action(async () => { - const { session, user } = await getCurrentSession(); - if (session === null) { - return { - error: "Not authenticated", - }; - } - - let verificationRequest = await getUserEmailVerificationRequestFromRequest(); - if (verificationRequest === null) { - if (user.emailVerified) { - return { - error: "Forbidden", - }; - } - - verificationRequest = await createEmailVerificationRequest( - user.id, - user.email - ); - } else { - verificationRequest = await createEmailVerificationRequest( - user.id, - verificationRequest.email - ); - } - await sendVerificationEmail( - verificationRequest.email, - verificationRequest.code - ); - await setEmailVerificationRequestCookie(verificationRequest); - return { - message: "A new code was sent to your inbox.", - }; -}); diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx deleted file mode 100644 index 3108c3fa..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx +++ /dev/null @@ -1,46 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { Button, Paper, PinInput, Stack, Text } from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; - -import { verifyEmailAction } from "./actions"; -import { emailVerificationSchema } from "./schema"; - -export default function LoginForm() { - const { form, handleSubmitWithAction, action } = useHookFormAction( - verifyEmailAction, - zodResolver(emailVerificationSchema), - {} - ); - - return ( -
- - - { - form.setValue("code", value); - if (value.length === 8) { - handleSubmitWithAction(); - } - }} - /> - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - -
- ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx deleted file mode 100644 index bfad170d..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { getUserEmailVerificationRequestFromRequest } from "@/server/auth/utils/email-verification"; -import { getRedirectCookie } from "@/server/auth/utils/redirect"; -import { getCurrentSession } from "@/server/auth/utils/session"; -import { Anchor, Container, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; - -import EmailVerificationForm from "./email-verification-form"; -import ResendButton from "./resend-button"; - -export default async function Page() { - const { user } = await getCurrentSession(); - - if (user === null) { - return redirect("/auth/login"); - } - - // TODO: Ideally we'd sent a new verification email automatically if the previous one is expired, - // but we can't set cookies inside server components. - const verificationRequest = - await getUserEmailVerificationRequestFromRequest(); - if (verificationRequest === null && user.emailVerified) { - const redirectTo = await getRedirectCookie(); - return redirect(redirectTo); - } - - return ( - - Verify your email - - Enter the code sent to {verificationRequest?.email ?? user.email} - - - Change email - - - - - - ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx deleted file mode 100644 index ee36ae70..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx +++ /dev/null @@ -1,37 +0,0 @@ -"use client"; - -import { Alert, Anchor, Button, Group, Stack, Text } from "@mantine/core"; -import { useAction } from "next-safe-action/hooks"; - -import { resendEmailVerificationAction } from "./actions"; - -export default function ResendButton() { - const action = useAction(resendEmailVerificationAction); - return ( - - - - {"Didn't receive the email?"} - - - - - {action.result.data?.message && ( - {action.result.data.message} - )} - - {action.result.data?.error && ( - - {action.result.data.error} - - )} - - ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts b/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts deleted file mode 100644 index d962f424..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from "zod/v4"; - -export const emailVerificationSchema = z.object({ - code: z.string().length(8), -}); diff --git a/packages/cli-old/template/extras/fmaddon-auth/components/auth/actions.ts b/packages/cli-old/template/extras/fmaddon-auth/components/auth/actions.ts deleted file mode 100644 index c4e4c11f..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/components/auth/actions.ts +++ /dev/null @@ -1,19 +0,0 @@ -"use server"; - -import { - getCurrentSession, - invalidateSession, -} from "@/server/auth/utils/session"; -import { redirect } from "next/navigation"; - -export async function currentSessionAction() { - return await getCurrentSession(); -} - -export async function logoutAction() { - const { session } = await currentSessionAction(); - if (session) { - await invalidateSession(session.id); - } - redirect("/"); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/components/auth/protect.tsx b/packages/cli-old/template/extras/fmaddon-auth/components/auth/protect.tsx deleted file mode 100644 index 9bce1e21..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/components/auth/protect.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { getCurrentSession } from "@/server/auth/utils/session"; - -import AuthRedirect from "./redirect"; - -/** - * This server component will protect the contents of it's children from users who aren't logged in - * It will redirect to the login page if the user is not logged in, or the verify email page if the user is logged in but hasn't verified their email - */ -export default async function Protect({ - children, -}: { - children: React.ReactNode; -}) { - const { session, user } = await getCurrentSession(); - if (!session) return ; - if (!user.emailVerified) return ; - return <>{children}; -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/components/auth/redirect.tsx b/packages/cli-old/template/extras/fmaddon-auth/components/auth/redirect.tsx deleted file mode 100644 index 40a2afef..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/components/auth/redirect.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"use client"; - -import { Center, Loader } from "@mantine/core"; -import Cookies from "js-cookie"; -import { redirect } from "next/navigation"; -import { useEffect } from "react"; - -/** - * A client-side component that redirects to the given path, but saves the current path in the redirectTo cookie. - */ -export default function AuthRedirect({ path }: { path: string }) { - useEffect(() => { - if (typeof window !== "undefined") { - Cookies.set("redirectTo", window.location.pathname, { - expires: 1 / 24 / 60, // 1 hour - }); - redirect(path); - } - }, []); - - return ( -
- -
- ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/components/auth/use-user.ts b/packages/cli-old/template/extras/fmaddon-auth/components/auth/use-user.ts deleted file mode 100644 index 46bec5b2..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/components/auth/use-user.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Session } from "@/server/auth/utils/session"; -import { User } from "@/server/auth/utils/user"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; - -import { currentSessionAction, logoutAction } from "./actions"; - -type LogoutAction = () => Promise; -type UseUserResult = - | { - state: "authenticated"; - session: Session; - user: User; - logout: LogoutAction; - } - | { - state: "unauthenticated"; - session: null; - user: null; - logout: LogoutAction; - } - | { state: "loading"; session: null; user: null; logout: LogoutAction }; - -export function useUser(): UseUserResult { - const query = useQuery({ - queryKey: ["current-user"], - queryFn: () => currentSessionAction(), - retry: false, - }); - const queryClient = useQueryClient(); - - const { mutateAsync } = useMutation({ - mutationFn: logoutAction, - onMutate: async () => { - await queryClient.cancelQueries({ queryKey: ["current-user"] }); - queryClient.setQueryData(["current-user"], { session: null, user: null }); - }, - onSettled: () => - queryClient.invalidateQueries({ queryKey: ["current-user"] }), - }); - - const defaultResult: UseUserResult = { - state: "unauthenticated", - session: null, - user: null, - logout: mutateAsync, - }; - - if (query.isLoading) { - return { ...defaultResult, state: "loading" }; - } - if (query.data?.session) { - return { - ...defaultResult, - state: "authenticated", - session: query.data.session, - user: query.data.user, - }; - } - return defaultResult; -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/components/auth/user-menu.tsx b/packages/cli-old/template/extras/fmaddon-auth/components/auth/user-menu.tsx deleted file mode 100644 index e4fd0778..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/components/auth/user-menu.tsx +++ /dev/null @@ -1,52 +0,0 @@ -"use client"; - -import { Button, Menu, px, Skeleton } from "@mantine/core"; -import { IconChevronDown, IconLogout, IconUser } from "@tabler/icons-react"; -import Link from "next/link"; - -import { useUser } from "./use-user"; - -export default function UserMenu() { - const { state, session, user, logout } = useUser(); - - if (state === "loading") { - return ; - } - if (state === "unauthenticated") { - return ( - - ); - } - return ( - - - - - - } - > - My Profile - - - } - onClick={logout} - > - Sign out - - - - ); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/emails/auth-code.tsx b/packages/cli-old/template/extras/fmaddon-auth/emails/auth-code.tsx deleted file mode 100644 index 3661a457..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/emails/auth-code.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { Body, Container, Head, Heading, Html, Img, Section, Text } from "@react-email/components"; - -interface AuthCodeEmailProps { - validationCode: string; - type: "verification" | "password-reset"; -} - -export const AuthCodeEmail = ({ validationCode, type }: AuthCodeEmailProps) => ( - - - - - ProofKit - {type === "verification" ? "Verify Your Email" : "Reset Your Password"} - - Enter the following code to {type === "verification" ? "verify your email" : "reset your password"} - -
- {validationCode} -
- If you did not request this code, you can ignore this email. -
- - -); - -AuthCodeEmail.PreviewProps = { - validationCode: "D7CU4GOV", - type: "verification", -} as AuthCodeEmailProps; - -export default AuthCodeEmail; - -const main = { - backgroundColor: "#ffffff", - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", -}; - -const container = { - backgroundColor: "#ffffff", - border: "1px solid #eee", - borderRadius: "5px", - boxShadow: "0 5px 10px rgba(20,50,70,.2)", - marginTop: "20px", - maxWidth: "360px", - margin: "0 auto", - padding: "68px 0 130px", -}; - -const logo: React.CSSProperties = { - margin: "0 auto", -}; - -const tertiary = { - color: "#0a85ea", - fontSize: "11px", - fontWeight: 700, - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", - height: "16px", - letterSpacing: "0", - lineHeight: "16px", - margin: "16px 8px 8px 8px", - textTransform: "uppercase" as const, - textAlign: "center" as const, -}; - -const secondary = { - color: "#000", - display: "inline-block", - fontFamily: "HelveticaNeue-Medium,Helvetica,Arial,sans-serif", - fontSize: "20px", - fontWeight: 500, - lineHeight: "24px", - marginBottom: "0", - marginTop: "0", - textAlign: "center" as const, - padding: "0 40px", -}; - -const codeContainer = { - background: "rgba(0,0,0,.05)", - borderRadius: "4px", - margin: "16px auto 14px", - verticalAlign: "middle", - width: "280px", -}; - -const code = { - color: "#000", - display: "inline-block", - fontFamily: "HelveticaNeue-Bold", - fontSize: "32px", - fontWeight: 700, - letterSpacing: "6px", - lineHeight: "40px", - paddingBottom: "8px", - paddingTop: "8px", - margin: "0 auto", - width: "100%", - textAlign: "center" as const, -}; - -const paragraph = { - color: "#444", - fontSize: "15px", - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", - letterSpacing: "0", - lineHeight: "23px", - padding: "0 40px", - margin: "0", - textAlign: "center" as const, -}; - -const link = { - color: "#444", - textDecoration: "underline", -}; - -const footer = { - color: "#000", - fontSize: "12px", - fontWeight: 800, - letterSpacing: "0", - lineHeight: "23px", - margin: "0", - marginTop: "20px", - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", - textAlign: "center" as const, - textTransform: "uppercase" as const, -}; diff --git a/packages/cli-old/template/extras/fmaddon-auth/middleware.ts b/packages/cli-old/template/extras/fmaddon-auth/middleware.ts deleted file mode 100644 index 86ea06f7..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/middleware.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NextResponse } from "next/server"; -import type { NextRequest } from "next/server"; - -export async function middleware(request: NextRequest): Promise { - if (request.method === "GET") { - const response = NextResponse.next(); - const token = request.cookies.get("session")?.value ?? null; - if (token !== null) { - // Only extend cookie expiration on GET requests since we can be sure - // a new session wasn't set when handling the request. - response.cookies.set("session", token, { - path: "/", - maxAge: 60 * 60 * 24 * 30, - sameSite: "lax", - httpOnly: true, - secure: process.env.NODE_ENV === "production", - }); - } - return response; - } - - const originHeader = request.headers.get("Origin"); - // NOTE: You may need to use `X-Forwarded-Host` instead - const hostHeader = request.headers.get("Host"); - if (originHeader === null || hostHeader === null) { - return new NextResponse(null, { - status: 403, - }); - } - let origin: URL; - try { - origin = new URL(originHeader); - } catch { - return new NextResponse(null, { - status: 403, - }); - } - if (origin.host !== hostHeader) { - return new NextResponse(null, { - status: 403, - }); - } - return NextResponse.next(); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts deleted file mode 100644 index 253eab74..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { encodeBase32 } from "@oslojs/encoding"; -import { cookies } from "next/headers"; - -import { emailVerificationLayout } from "../db/client"; -import { TemailVerification } from "../db/emailVerification"; -import { sendEmail } from "../email"; -import { generateRandomOTP } from "./index"; -import { getCurrentSession } from "./session"; - -/** - * An Email Verification Request is a record in the email verification table that is created when a user requests to change their email address. It's like a temporary session which can expire if the user doesn't verify the new email address within a certain amount of time. - */ - -/** - * Get a user's email verification request. - * @param userId - The ID of the user. - * @param id - The ID of the email verification request. - * @returns The email verification request, or null if it doesn't exist. - */ -export async function getUserEmailVerificationRequest( - userId: string, - id: string -): Promise { - const result = await emailVerificationLayout.maybeFindFirst({ - query: { id_user: `==${userId}`, id: `==${id}` }, - }); - return result?.data.fieldData ?? null; -} - -/** - * Create a new email verification request for a user. - * @param id_user - The ID of the user. - * @param email - The email address to verify. - * @returns The email verification request. - */ -export async function createEmailVerificationRequest( - id_user: string, - email: string -): Promise { - deleteUserEmailVerificationRequest(id_user); - const idBytes = new Uint8Array(20); - crypto.getRandomValues(idBytes); - const id = encodeBase32(idBytes).toLowerCase(); - - const code = generateRandomOTP(); - const expiresAt = new Date(Date.now() + 1000 * 60 * 10); - - const request: TemailVerification = { - id, - code, - expires_at: Math.floor(expiresAt.getTime() / 1000), - email, - id_user, - }; - - await emailVerificationLayout.create({ - fieldData: request, - }); - - return request; -} - -/** - * Delete a user's email verification request. - * @param id_user - The ID of the user. - */ -export async function deleteUserEmailVerificationRequest( - id_user: string -): Promise { - const result = await emailVerificationLayout.maybeFindFirst({ - query: { id_user: `==${id_user}` }, - }); - if (result === null) return; - - await emailVerificationLayout.delete({ recordId: result.data.recordId }); -} - -/** - * Send a verification email to a user. - * @param email - The email address to send the verification email to. - * @param code - The verification code to send to the user. - */ -export async function sendVerificationEmail( - email: string, - code: string -): Promise { - await sendEmail({ to: email, code, type: "verification" }); -} - -/** - * Set a cookie for a user's email verification request. - * @param request - The email verification request. - */ -export async function setEmailVerificationRequestCookie( - request: TemailVerification -): Promise { - (await cookies()).set("email_verification", request.id, { - httpOnly: true, - path: "/", - secure: process.env.NODE_ENV === "production", - sameSite: "lax", - expires: request.expires_at - ? new Date(request.expires_at * 1000) - : new Date(Date.now() + 1000 * 60 * 60), - }); -} - -/** - * Delete the cookie for a user's email verification request. - */ -export async function deleteEmailVerificationRequestCookie(): Promise { - (await cookies()).set("email_verification", "", { - httpOnly: true, - path: "/", - secure: process.env.NODE_ENV === "production", - sameSite: "lax", - maxAge: 0, - }); -} - -/** - * Get a user's email verification request from the cookie. - * @returns The email verification request, or null if it doesn't exist. - */ -export async function getUserEmailVerificationRequestFromRequest(): Promise { - const { user } = await getCurrentSession(); - if (user === null) { - return null; - } - const id = (await cookies()).get("email_verification")?.value ?? null; - if (id === null) { - return null; - } - const request = await getUserEmailVerificationRequest(user.id, id); - - return request; -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/encryption.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/encryption.ts deleted file mode 100644 index 377f773d..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/encryption.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { createCipheriv, createDecipheriv } from "crypto"; -import { DynamicBuffer } from "@oslojs/binary"; -import { decodeBase64 } from "@oslojs/encoding"; - -const key = decodeBase64(process.env.ENCRYPTION_KEY ?? ""); - -export function encrypt(data: Uint8Array): Uint8Array { - const iv = new Uint8Array(16); - crypto.getRandomValues(iv); - const cipher = createCipheriv("aes-128-gcm", key, iv); - const encrypted = new DynamicBuffer(0); - encrypted.write(iv); - encrypted.write(cipher.update(data)); - encrypted.write(cipher.final()); - encrypted.write(cipher.getAuthTag()); - return encrypted.bytes(); -} - -/** - * Encrypt a string for storage in the database. - * Here we're returning a base64 encoded string since FileMaker doesn't store binary data. - * @param data - The string to encrypt. - * @returns The encrypted string. - */ -export function encryptString(data: string): string { - const encrypted = encrypt(new TextEncoder().encode(data)); - return Buffer.from(encrypted).toString("base64"); -} - -/** - * Decrypt a string stored in the database. - * @param encrypted - The encrypted string to decrypt. - * @returns The decrypted string. - */ -export function decrypt(encrypted: Uint8Array): Uint8Array { - if (encrypted.byteLength < 33) { - throw new Error("Invalid data"); - } - const decipher = createDecipheriv("aes-128-gcm", key, encrypted.slice(0, 16)); - decipher.setAuthTag(encrypted.slice(encrypted.byteLength - 16)); - const decrypted = new DynamicBuffer(0); - decrypted.write( - decipher.update(encrypted.slice(16, encrypted.byteLength - 16)) - ); - decrypted.write(decipher.final()); - return decrypted.bytes(); -} - -export function decryptToString(data: Uint8Array): string { - return new TextDecoder().decode(decrypt(data)); -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/index.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/index.ts deleted file mode 100644 index 41849aef..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { encodeBase32UpperCaseNoPadding } from "@oslojs/encoding"; - -export function generateRandomOTP(): string { - const bytes = new Uint8Array(5); - crypto.getRandomValues(bytes); - const code = encodeBase32UpperCaseNoPadding(bytes); - return code; -} - -export const options = { - password: { - minLength: 8, - maxLength: 255, - checkCompromised: false, // set to true to prevent known compromised passwords on signup - }, -}; diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts deleted file mode 100644 index d3434460..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { sha256 } from "@oslojs/crypto/sha2"; -import { encodeHexLowerCase } from "@oslojs/encoding"; -import { cookies } from "next/headers"; - -import { passwordResetLayout } from "../db/client"; -import { TpasswordReset } from "../db/passwordReset"; -import { sendEmail } from "../email"; -import { generateRandomOTP } from "./index"; -import type { User } from "./user"; - -type PasswordResetSession = Omit< - TpasswordReset, - | "proofkit_auth_users::email" - | "proofkit_auth_users::emailVerified" - | "proofkit_auth_users::username" ->; - -export async function createPasswordResetSession( - token: string, - id_user: string, - email: string -): Promise { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const session: PasswordResetSession = { - id: sessionId, - id_user, - email, - expires_at: Math.floor( - new Date(Date.now() + 1000 * 60 * 10).getTime() / 1000 - ), - code: generateRandomOTP(), - email_verified: 0, - }; - await passwordResetLayout.create({ fieldData: session }); - - return session; -} - -/** - * Validate a password reset session token. - * @param token - The password reset session token. - * @returns The password reset session, or null if it doesn't exist. - */ -export async function validatePasswordResetSessionToken( - token: string -): Promise { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const row = await passwordResetLayout.maybeFindFirst({ - query: { id: `==${sessionId}` }, - }); - - if (row === null) { - return { session: null, user: null }; - } - const session: PasswordResetSession = { - id: row.data.fieldData.id, - id_user: row.data.fieldData.id_user, - email: row.data.fieldData.email, - code: row.data.fieldData.code, - expires_at: row.data.fieldData.expires_at, - email_verified: row.data.fieldData.email_verified, - }; - - const user: User = { - id: row.data.fieldData.id_user, - email: row.data.fieldData["proofkit_auth_users::email"], - username: row.data.fieldData["proofkit_auth_users::username"], - emailVerified: Boolean( - row.data.fieldData["proofkit_auth_users::emailVerified"] - ), - }; - if (session.expires_at && Date.now() >= session.expires_at * 1000) { - await passwordResetLayout.delete({ recordId: row.data.recordId }); - return { session: null, user: null }; - } - return { session, user }; -} - -async function fetchPasswordResetSession(sessionId: string) { - return ( - await passwordResetLayout.findOne({ query: { id: `==${sessionId}` } }) - ).data; -} - -export async function setPasswordResetSessionAsEmailVerified( - sessionId: string -): Promise { - const { recordId } = await fetchPasswordResetSession(sessionId); - await passwordResetLayout.update({ - recordId, - fieldData: { email_verified: 1 }, - }); -} - -export async function invalidateUserPasswordResetSessions( - userId: string -): Promise { - const sessions = await passwordResetLayout.find({ - query: { id_user: `==${userId}` }, - ignoreEmptyResult: true, - }); - for (const session of sessions.data) { - await passwordResetLayout.delete({ recordId: session.recordId }); - } -} - -export async function validatePasswordResetSessionRequest(): Promise { - const token = (await cookies()).get("password_reset_session")?.value ?? null; - if (token === null) { - return { session: null, user: null }; - } - const result = await validatePasswordResetSessionToken(token); - if (result.session === null) { - deletePasswordResetSessionTokenCookie(); - } - return result; -} - -export async function setPasswordResetSessionTokenCookie( - token: string, - expiresAt: number | null -): Promise { - (await cookies()).set("password_reset_session", token, { - expires: expiresAt - ? new Date(expiresAt * 1000) - : new Date(Date.now() + 60 * 60 * 1000), - sameSite: "lax", - httpOnly: true, - path: "/", - secure: process.env.NODE_ENV === "production", - }); -} - -export async function deletePasswordResetSessionTokenCookie(): Promise { - (await cookies()).set("password_reset_session", "", { - maxAge: 0, - sameSite: "lax", - httpOnly: true, - path: "/", - secure: process.env.NODE_ENV === "production", - }); -} - -export async function sendPasswordResetEmail( - email: string, - code: string -): Promise { - await sendEmail({ to: email, code, type: "password-reset" }); -} - -export type PasswordResetSessionValidationResult = - | { session: PasswordResetSession; user: User } - | { session: null; user: null }; diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password.ts deleted file mode 100644 index bf723a6f..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/password.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { options } from "."; -import { hash, verify } from "@node-rs/argon2"; -import { sha1 } from "@oslojs/crypto/sha1"; -import { encodeHexLowerCase } from "@oslojs/encoding"; - -/** - * Hash a password using Argon2. - * @param password - The password to hash. - * @returns The hashed password. - */ -export async function hashPassword(password: string): Promise { - return await hash(password, { - memoryCost: 19456, - timeCost: 2, - outputLen: 32, - parallelism: 1, - }); -} - -/** - * Verify that a password matches a hash. - * @param hash - The hash to verify against. - * @param password - The password to verify. - * @returns True if the password matches the hash, false otherwise. - */ -export async function verifyPasswordHash( - hash: string, - password: string -): Promise { - return await verify(hash, password); -} - -/** - * Verify that a password is strong enough. - * @param password - The password to verify. - * @returns True if the password is strong enough, false otherwise. - */ -export async function verifyPasswordStrength( - password: string -): Promise { - if ( - password.length < options.password.minLength || - password.length > options.password.maxLength - ) { - return false; - } - - if (options.password.checkCompromised) { - const hash = encodeHexLowerCase(sha1(new TextEncoder().encode(password))); - const hashPrefix = hash.slice(0, 5); - const response = await fetch( - `https://api.pwnedpasswords.com/range/${hashPrefix}` - ); - const data = await response.text(); - const items = data.split("\n"); - for (const item of items) { - const hashSuffix = item.slice(0, 35).toLowerCase(); - if (hash === hashPrefix + hashSuffix) { - console.log( - "User's new password was found in list of compromised passwords, reject" - ); - return false; - } - } - } - return true; -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/redirect.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/redirect.ts deleted file mode 100644 index eb3b467b..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/redirect.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { cookies } from "next/headers"; - -export async function getRedirectCookie() { - const cookieStore = await cookies(); - const redirectTo = cookieStore.get("redirectTo")?.value; - cookieStore.delete("redirectTo"); - return redirectTo ?? "/"; -} diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/session.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/session.ts deleted file mode 100644 index aaa80f35..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/session.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { sha256 } from "@oslojs/crypto/sha2"; -import { - encodeBase32LowerCaseNoPadding, - encodeHexLowerCase, -} from "@oslojs/encoding"; -import { cookies } from "next/headers"; -import { cache } from "react"; - -import { sessionsLayout } from "../db/client"; -import { Tsessions as _Session } from "../db/sessions"; -import type { User } from "./user"; - -/** - * Generate a random session token with sufficient entropy for a session ID. - * @returns The session token. - */ -export function generateSessionToken(): string { - const bytes = new Uint8Array(20); - crypto.getRandomValues(bytes); - const token = encodeBase32LowerCaseNoPadding(bytes); - return token; -} - -/** - * Create a new session for a user and save it to the database. - * @param token - The session token. - * @param userId - The ID of the user. - * @returns The session. - */ -export async function createSession( - token: string, - userId: string -): Promise { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const session: Session = { - id: sessionId, - id_user: userId, - expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), - }; - - // create session in DB - await sessionsLayout.create({ - fieldData: { - id: session.id, - id_user: session.id_user, - expiresAt: Math.floor(session.expiresAt.getTime() / 1000), - }, - }); - - return session; -} - -/** - * Invalidate a session by deleting it from the database. - * @param sessionId - The ID of the session to invalidate. - */ -export async function invalidateSession(sessionId: string): Promise { - const fmResult = await sessionsLayout.maybeFindFirst({ - query: { id: `==${sessionId}` }, - }); - if (fmResult === null) { - return; - } - await sessionsLayout.delete({ recordId: fmResult.data.recordId }); -} - -/** - * Validate a session token to make sure it still exists in the database and hasn't expired. - * @param token - The session token. - * @returns The session, or null if it doesn't exist. - */ -export async function validateSessionToken( - token: string -): Promise { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - - const result = await sessionsLayout.maybeFindFirst({ - query: { id: `==${sessionId}` }, - }); - if (result === null) { - return { session: null, user: null }; - } - - const fmResult = result.data.fieldData; - const recordId = result.data.recordId; - const session: Session = { - id: fmResult.id, - id_user: fmResult.id_user, - expiresAt: fmResult.expiresAt - ? new Date(fmResult.expiresAt * 1000) - : new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), - }; - - const user: User = { - id: session.id_user, - email: fmResult["proofkit_auth_users::email"], - emailVerified: Boolean(fmResult["proofkit_auth_users::emailVerified"]), - username: fmResult["proofkit_auth_users::username"], - }; - - // delete session if it has expired - if (Date.now() >= session.expiresAt.getTime()) { - await sessionsLayout.delete({ recordId }); - return { session: null, user: null }; - } - - // extend session if it's going to expire soon - // You may want to customize this logic to better suit your app's requirements - if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) { - session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30); - await sessionsLayout.update({ - recordId, - fieldData: { - expiresAt: Math.floor(session.expiresAt.getTime() / 1000), - }, - }); - } - - return { session, user }; -} - -/** - * Get the current session from the cookie. - * Wrapped in a React cache to avoid calling the database more than once per request - * This function can be used in server components, server actions, and route handlers (but importantly not middleware). - * @returns The session, or null if it doesn't exist. - */ -export const getCurrentSession = cache( - async (): Promise => { - const token = (await cookies()).get("session")?.value ?? null; - if (token === null) { - return { session: null, user: null }; - } - const result = await validateSessionToken(token); - return result; - } -); - -/** - * Invalidate all sessions for a user by deleting them from the database. - * @param userId - The ID of the user. - */ -export async function invalidateUserSessions(userId: string): Promise { - const sessions = await sessionsLayout.findAll({ - query: { id_user: `==${userId}` }, - }); - for (const session of sessions) { - await sessionsLayout.delete({ recordId: session.recordId }); - } -} - -/** - * Set a cookie for a session. - * @param token - The session token. - * @param expiresAt - The expiration date of the session. - */ -export async function setSessionTokenCookie( - token: string, - expiresAt: Date -): Promise { - (await cookies()).set("session", token, { - httpOnly: true, - path: "/", - secure: process.env.NODE_ENV === "production", - sameSite: "lax", - expires: expiresAt, - }); -} - -/** - * Delete the session cookie. - */ -export async function deleteSessionTokenCookie(): Promise { - (await cookies()).set("session", "", { - httpOnly: true, - path: "/", - secure: process.env.NODE_ENV === "production", - sameSite: "lax", - maxAge: 0, - }); -} - -export interface Session { - id: string; - expiresAt: Date; - id_user: string; -} - -type SessionValidationResult = - | { session: Session; user: User } - | { session: null; user: null }; diff --git a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/user.ts b/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/user.ts deleted file mode 100644 index 1b7e0194..00000000 --- a/packages/cli-old/template/extras/fmaddon-auth/server/auth/utils/user.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { usersLayout } from "../db/client"; -import { Tusers as _User } from "../db/users"; -import { hashPassword, verifyPasswordHash } from "./password"; - -export type User = Partial< - Omit<_User, "id" | "password_hash" | "recovery_code" | "emailVerified"> -> & { - id: string; - email: string; - emailVerified: boolean; -}; - -/** An internal helper function to fetch a user from the database. */ -async function fetchUser(userId: string) { - const { data } = await usersLayout.findOne({ - query: { id: `==${userId}` }, - }); - return data; -} - -/** Create a new user in the database. */ -export async function createUser( - email: string, - password: string -): Promise { - const password_hash = await hashPassword(password); - const { recordId } = await usersLayout.create({ - fieldData: { - email, - password_hash, - emailVerified: 0, - }, - }); - const fmResult = await usersLayout.get({ recordId }); - const { fieldData } = fmResult.data[0]; - - const user: User = { - id: fieldData.id, - email, - emailVerified: false, - username: "", - }; - return user; -} - -/** Update a user's password in the database. */ -export async function updateUserPassword( - userId: string, - password: string -): Promise { - const password_hash = await hashPassword(password); - const { recordId } = await fetchUser(userId); - - await usersLayout.update({ recordId, fieldData: { password_hash } }); -} - -export async function updateUserEmailAndSetEmailAsVerified( - userId: string, - email: string -): Promise { - const { recordId } = await fetchUser(userId); - await usersLayout.update({ - recordId, - fieldData: { email, emailVerified: 1 }, - }); -} - -export async function setUserAsEmailVerifiedIfEmailMatches( - userId: string, - email: string -): Promise { - try { - const { - data: { recordId }, - } = await usersLayout.findOne({ - query: { id: `==${userId}`, email: `==${email}` }, - }); - await usersLayout.update({ recordId, fieldData: { emailVerified: 1 } }); - return true; - } catch (error) { - return false; - } -} - -export async function getUserFromEmail(email: string): Promise { - const fmResult = await usersLayout.maybeFindFirst({ - query: { email: `==${email}` }, - }); - if (fmResult === null) return null; - - const { - data: { fieldData }, - } = fmResult; - - const user: User = { - id: fieldData.id, - email: fieldData.email, - emailVerified: Boolean(fieldData.emailVerified), - username: fieldData.username, - }; - return user; -} - -/** - * Validate a user's email/password combination. - * @param email - The user's email. - * @param password - The user's password. - * @returns The user, or null if the login is invalid. - */ -export async function validateLogin( - email: string, - password: string -): Promise { - try { - const { - data: { fieldData }, - } = await usersLayout.findOne({ - query: { email: `==${email}` }, - }); - - const validPassword = await verifyPasswordHash( - fieldData.password_hash, - password - ); - if (!validPassword) { - return null; - } - const user: User = { - id: fieldData.id, - email: fieldData.email, - emailVerified: Boolean(fieldData.emailVerified), - username: fieldData.username, - }; - return user; - } catch (error) { - return null; - } -} - -export async function checkEmailAvailability(email: string): Promise { - const { data } = await usersLayout.find({ - query: { email: `==${email}` }, - ignoreEmptyResult: true, - }); - return data.length === 0; -} diff --git a/packages/cli-old/template/extras/prisma/schema/base-planetscale.prisma b/packages/cli-old/template/extras/prisma/schema/base-planetscale.prisma deleted file mode 100644 index 6b9dd139..00000000 --- a/packages/cli-old/template/extras/prisma/schema/base-planetscale.prisma +++ /dev/null @@ -1,24 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" - previewFeatures = ["driverAdapters"] -} - -datasource db { - provider = "mysql" - url = env("DATABASE_URL") - - // If you have enabled foreign key constraints for your database, remove this line. - relationMode = "prisma" -} - -model Post { - id Int @id @default(autoincrement()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([name]) -} diff --git a/packages/cli-old/template/extras/prisma/schema/base.prisma b/packages/cli-old/template/extras/prisma/schema/base.prisma deleted file mode 100644 index ddb6e099..00000000 --- a/packages/cli-old/template/extras/prisma/schema/base.prisma +++ /dev/null @@ -1,20 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "sqlite" - url = env("DATABASE_URL") -} - -model Post { - id Int @id @default(autoincrement()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([name]) -} diff --git a/packages/cli-old/template/extras/prisma/schema/with-auth-planetscale.prisma b/packages/cli-old/template/extras/prisma/schema/with-auth-planetscale.prisma deleted file mode 100644 index 198915b9..00000000 --- a/packages/cli-old/template/extras/prisma/schema/with-auth-planetscale.prisma +++ /dev/null @@ -1,77 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" - previewFeatures = ["driverAdapters"] -} - -datasource db { - provider = "mysql" - url = env("DATABASE_URL") - - // If you have enabled foreign key constraints for your database, remove this line. - relationMode = "prisma" -} - -model Post { - id Int @id @default(autoincrement()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - createdBy User @relation(fields: [createdById], references: [id]) - createdById String - - @@index([name]) - @@index([createdById]) -} - -// Necessary for Next auth -model Account { - id String @id @default(cuid()) - userId String - type String - provider String - providerAccountId String - refresh_token String? @db.Text - access_token String? @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? @db.Text - session_state String? - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([provider, providerAccountId]) - @@index([userId]) -} - -model Session { - id String @id @default(cuid()) - sessionToken String @unique - userId String - expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@index([userId]) -} - -model User { - id String @id @default(cuid()) - name String? - email String? @unique - emailVerified DateTime? - image String? - accounts Account[] - sessions Session[] - posts Post[] -} - -model VerificationToken { - identifier String - token String @unique - expires DateTime - - @@unique([identifier, token]) -} diff --git a/packages/cli-old/template/extras/prisma/schema/with-auth.prisma b/packages/cli-old/template/extras/prisma/schema/with-auth.prisma deleted file mode 100644 index b17831e6..00000000 --- a/packages/cli-old/template/extras/prisma/schema/with-auth.prisma +++ /dev/null @@ -1,74 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "sqlite" - // NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below - // Further reading: - // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema - // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string - url = env("DATABASE_URL") -} - -model Post { - id Int @id @default(autoincrement()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - createdBy User @relation(fields: [createdById], references: [id]) - createdById String - - @@index([name]) -} - -// Necessary for Next auth -model Account { - id String @id @default(cuid()) - userId String - type String - provider String - providerAccountId String - refresh_token String? // @db.Text - access_token String? // @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? // @db.Text - session_state String? - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - refresh_token_expires_in Int? - - @@unique([provider, providerAccountId]) -} - -model Session { - id String @id @default(cuid()) - sessionToken String @unique - userId String - expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) -} - -model User { - id String @id @default(cuid()) - name String? - email String? @unique - emailVerified DateTime? - image String? - accounts Account[] - sessions Session[] - posts Post[] -} - -model VerificationToken { - identifier String - token String @unique - expires DateTime - - @@unique([identifier, token]) -} diff --git a/packages/cli-old/template/extras/src/app/_components/post-tw.tsx b/packages/cli-old/template/extras/src/app/_components/post-tw.tsx deleted file mode 100644 index ebe15eab..00000000 --- a/packages/cli-old/template/extras/src/app/_components/post-tw.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"use client"; - -import { useState } from "react"; - -import { api } from "~/trpc/react"; - -export function LatestPost() { - const [latestPost] = api.post.getLatest.useSuspenseQuery(); - - const utils = api.useUtils(); - const [name, setName] = useState(""); - const createPost = api.post.create.useMutation({ - onSuccess: async () => { - await utils.post.invalidate(); - setName(""); - }, - }); - - return ( -
- {latestPost ? ( -

Your most recent post: {latestPost.name}

- ) : ( -

You have no posts yet.

- )} -
{ - e.preventDefault(); - createPost.mutate({ name }); - }} - className="flex flex-col gap-2" - > - setName(e.target.value)} - className="w-full rounded-full px-4 py-2 text-black" - /> - -
-
- ); -} diff --git a/packages/cli-old/template/extras/src/app/_components/post.tsx b/packages/cli-old/template/extras/src/app/_components/post.tsx deleted file mode 100644 index 1ad81347..00000000 --- a/packages/cli-old/template/extras/src/app/_components/post.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import { useState } from "react"; - -import { api } from "~/trpc/react"; -import styles from "../index.module.css"; - -export function LatestPost() { - const [latestPost] = api.post.getLatest.useSuspenseQuery(); - - const utils = api.useUtils(); - const [name, setName] = useState(""); - const createPost = api.post.create.useMutation({ - onSuccess: async () => { - await utils.post.invalidate(); - setName(""); - }, - }); - - return ( -
- {latestPost ? ( -

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

- ) : ( -

You have no posts yet.

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

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

- -
-

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

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

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

- -
-

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

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

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

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

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

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

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

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

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

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

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

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

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

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

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

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

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

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

-
- Learn more about Create T3 App, the libraries it uses, and how - to deploy it. -
- -
-
-
- - ); -} diff --git a/packages/cli-old/template/extras/src/server/api/root.ts b/packages/cli-old/template/extras/src/server/api/root.ts deleted file mode 100644 index b341fc4d..00000000 --- a/packages/cli-old/template/extras/src/server/api/root.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { postRouter } from "~/server/api/routers/post"; -import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc"; - -/** - * This is the primary router for your server. - * - * All routers added in /api/routers should be manually added here. - */ -export const appRouter = createTRPCRouter({ - post: postRouter, -}); - -// export type definition of API -export type AppRouter = typeof appRouter; - -/** - * Create a server-side caller for the tRPC API. - * @example - * const trpc = createCaller(createContext); - * const res = await trpc.post.all(); - * ^? Post[] - */ -export const createCaller = createCallerFactory(appRouter); diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/base.ts b/packages/cli-old/template/extras/src/server/api/routers/post/base.ts deleted file mode 100644 index 6781c531..00000000 --- a/packages/cli-old/template/extras/src/server/api/routers/post/base.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { z } from "zod/v4"; - -import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; - -// Mocked DB -interface Post { - id: number; - name: string; -} -const posts: Post[] = [ - { - id: 1, - name: "Hello World", - }, -]; - -export const postRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - - create: publicProcedure - .input(z.object({ name: z.string().min(1) })) - .mutation(async ({ input }) => { - const post: Post = { - id: posts.length + 1, - name: input.name, - }; - posts.push(post); - return post; - }), - - getLatest: publicProcedure.query(() => { - return posts.at(-1) ?? null; - }), -}); diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-drizzle.ts b/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-drizzle.ts deleted file mode 100644 index 35ac7ba8..00000000 --- a/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-drizzle.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { z } from "zod/v4"; - -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from "~/server/api/trpc"; -import { posts } from "~/server/db/schema"; - -export const postRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - - create: protectedProcedure - .input(z.object({ name: z.string().min(1) })) - .mutation(async ({ ctx, input }) => { - await ctx.db.insert(posts).values({ - name: input.name, - createdById: ctx.session.user.id, - }); - }), - - getLatest: publicProcedure.query(async ({ ctx }) => { - const post = await ctx.db.query.posts.findFirst({ - orderBy: (posts, { desc }) => [desc(posts.createdAt)], - }); - - return post ?? null; - }), - - getSecretMessage: protectedProcedure.query(() => { - return "you can now see this secret message!"; - }), -}); diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-prisma.ts b/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-prisma.ts deleted file mode 100644 index f0140b76..00000000 --- a/packages/cli-old/template/extras/src/server/api/routers/post/with-auth-prisma.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { z } from "zod/v4"; - -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from "~/server/api/trpc"; - -export const postRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - - create: protectedProcedure - .input(z.object({ name: z.string().min(1) })) - .mutation(async ({ ctx, input }) => { - return ctx.db.post.create({ - data: { - name: input.name, - createdBy: { connect: { id: ctx.session.user.id } }, - }, - }); - }), - - getLatest: protectedProcedure.query(async ({ ctx }) => { - const post = await ctx.db.post.findFirst({ - orderBy: { createdAt: "desc" }, - where: { createdBy: { id: ctx.session.user.id } }, - }); - - return post ?? null; - }), - - getSecretMessage: protectedProcedure.query(() => { - return "you can now see this secret message!"; - }), -}); diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/with-auth.ts b/packages/cli-old/template/extras/src/server/api/routers/post/with-auth.ts deleted file mode 100644 index 8ea389bf..00000000 --- a/packages/cli-old/template/extras/src/server/api/routers/post/with-auth.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { z } from "zod/v4"; - -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from "~/server/api/trpc"; - -let post = { - id: 1, - name: "Hello World", -}; - -export const postRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - - create: protectedProcedure - .input(z.object({ name: z.string().min(1) })) - .mutation(async ({ input }) => { - post = { id: post.id + 1, name: input.name }; - return post; - }), - - getLatest: protectedProcedure.query(() => { - return post; - }), - - getSecretMessage: protectedProcedure.query(() => { - return "you can now see this secret message!"; - }), -}); diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/with-drizzle.ts b/packages/cli-old/template/extras/src/server/api/routers/post/with-drizzle.ts deleted file mode 100644 index a295842c..00000000 --- a/packages/cli-old/template/extras/src/server/api/routers/post/with-drizzle.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { z } from "zod/v4"; - -import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; -import { posts } from "~/server/db/schema"; - -export const postRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - - create: publicProcedure - .input(z.object({ name: z.string().min(1) })) - .mutation(async ({ ctx, input }) => { - await ctx.db.insert(posts).values({ - name: input.name, - }); - }), - - getLatest: publicProcedure.query(async ({ ctx }) => { - const post = await ctx.db.query.posts.findFirst({ - orderBy: (posts, { desc }) => [desc(posts.createdAt)], - }); - - return post ?? null; - }), -}); diff --git a/packages/cli-old/template/extras/src/server/api/routers/post/with-prisma.ts b/packages/cli-old/template/extras/src/server/api/routers/post/with-prisma.ts deleted file mode 100644 index 3282fa6e..00000000 --- a/packages/cli-old/template/extras/src/server/api/routers/post/with-prisma.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { z } from "zod/v4"; - -import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; - -export const postRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - - create: publicProcedure - .input(z.object({ name: z.string().min(1) })) - .mutation(async ({ ctx, input }) => { - return ctx.db.post.create({ - data: { - name: input.name, - }, - }); - }), - - getLatest: publicProcedure.query(async ({ ctx }) => { - const post = await ctx.db.post.findFirst({ - orderBy: { createdAt: "desc" }, - }); - - return post ?? null; - }), -}); diff --git a/packages/cli-old/template/extras/src/server/api/trpc-app/base.ts b/packages/cli-old/template/extras/src/server/api/trpc-app/base.ts deleted file mode 100644 index e831d1a8..00000000 --- a/packages/cli-old/template/extras/src/server/api/trpc-app/base.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ -import { initTRPC } from "@trpc/server"; -import superjson from "superjson"; -import { ZodError } from "zod/v4"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - * - * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each - * wrap this and provides the required context. - * - * @see https://trpc.io/docs/server/context - */ -export const createTRPCContext = async (opts: { headers: Headers }) => { - return { - ...opts, - }; -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an articifial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth-db.ts b/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth-db.ts deleted file mode 100644 index a8ca5724..00000000 --- a/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth-db.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ - -import { initTRPC, TRPCError } from "@trpc/server"; -import superjson from "superjson"; -import { ZodError } from "zod/v4"; - -import { getServerAuthSession } from "~/server/auth"; -import { db } from "~/server/db"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - * - * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each - * wrap this and provides the required context. - * - * @see https://trpc.io/docs/server/context - */ -export const createTRPCContext = async (opts: { headers: Headers }) => { - const session = await getServerAuthSession(); - - return { - db, - session, - ...opts, - }; -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an articifial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); - -/** - * Protected (authenticated) procedure - * - * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies - * the session is valid and guarantees `ctx.session.user` is not null. - * - * @see https://trpc.io/docs/procedures - */ -export const protectedProcedure = t.procedure - .use(timingMiddleware) - .use(({ ctx, next }) => { - if (!ctx.session || !ctx.session.user) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - return next({ - ctx: { - // infers the `session` as non-nullable - session: { ...ctx.session, user: ctx.session.user }, - }, - }); - }); diff --git a/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth.ts b/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth.ts deleted file mode 100644 index 55e20cf1..00000000 --- a/packages/cli-old/template/extras/src/server/api/trpc-app/with-auth.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ -import { initTRPC, TRPCError } from "@trpc/server"; -import superjson from "superjson"; -import { ZodError } from "zod/v4"; - -import { getServerAuthSession } from "~/server/auth"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - * - * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each - * wrap this and provides the required context. - * - * @see https://trpc.io/docs/server/context - */ -export const createTRPCContext = async (opts: { headers: Headers }) => { - const session = await getServerAuthSession(); - - return { - session, - ...opts, - }; -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an articifial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); - -/** - * Protected (authenticated) procedure - * - * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies - * the session is valid and guarantees `ctx.session.user` is not null. - * - * @see https://trpc.io/docs/procedures - */ -export const protectedProcedure = t.procedure - .use(timingMiddleware) - .use(({ ctx, next }) => { - if (!ctx.session || !ctx.session.user) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - return next({ - ctx: { - // infers the `session` as non-nullable - session: { ...ctx.session, user: ctx.session.user }, - }, - }); - }); diff --git a/packages/cli-old/template/extras/src/server/api/trpc-app/with-db.ts b/packages/cli-old/template/extras/src/server/api/trpc-app/with-db.ts deleted file mode 100644 index c77a7530..00000000 --- a/packages/cli-old/template/extras/src/server/api/trpc-app/with-db.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ -import { initTRPC } from "@trpc/server"; -import superjson from "superjson"; -import { ZodError } from "zod/v4"; - -import { db } from "~/server/db"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - * - * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each - * wrap this and provides the required context. - * - * @see https://trpc.io/docs/server/context - */ -export const createTRPCContext = async (opts: { headers: Headers }) => { - return { - db, - ...opts, - }; -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an articifial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/packages/cli-old/template/extras/src/server/api/trpc-pages/base.ts b/packages/cli-old/template/extras/src/server/api/trpc-pages/base.ts deleted file mode 100644 index b1d9f34c..00000000 --- a/packages/cli-old/template/extras/src/server/api/trpc-pages/base.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ - -import { initTRPC } from "@trpc/server"; -import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; -import superjson from "superjson"; -import { ZodError } from "zod/v4"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - */ - -type CreateContextOptions = Record; - -/** - * This helper generates the "internals" for a tRPC context. If you need to use it, you can export - * it from here. - * - * Examples of things you may need it for: - * - testing, so we don't have to mock Next.js' req/res - * - tRPC's `createSSGHelpers`, where we don't have req/res - * - * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts - */ -const createInnerTRPCContext = (_opts: CreateContextOptions) => { - return {}; -}; - -/** - * This is the actual context you will use in your router. It will be used to process every request - * that goes through your tRPC endpoint. - * - * @see https://trpc.io/docs/context - */ -export const createTRPCContext = (_opts: CreateNextContextOptions) => { - return createInnerTRPCContext({}); -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ - -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an articifial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth-db.ts b/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth-db.ts deleted file mode 100644 index f057f427..00000000 --- a/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth-db.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ - -import { initTRPC, TRPCError } from "@trpc/server"; -import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; -import { type Session } from "next-auth"; -import superjson from "superjson"; -import { ZodError } from "zod/v4"; - -import { getServerAuthSession } from "~/server/auth"; -import { db } from "~/server/db"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - */ - -interface CreateContextOptions { - session: Session | null; -} - -/** - * This helper generates the "internals" for a tRPC context. If you need to use it, you can export - * it from here. - * - * Examples of things you may need it for: - * - testing, so we don't have to mock Next.js' req/res - * - tRPC's `createSSGHelpers`, where we don't have req/res - * - * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts - */ -const createInnerTRPCContext = (opts: CreateContextOptions) => { - return { - session: opts.session, - db, - }; -}; - -/** - * This is the actual context you will use in your router. It will be used to process every request - * that goes through your tRPC endpoint. - * - * @see https://trpc.io/docs/context - */ -export const createTRPCContext = async (opts: CreateNextContextOptions) => { - const { req, res } = opts; - - // Get the session from the server using the getServerSession wrapper function - const session = await getServerAuthSession({ req, res }); - - return createInnerTRPCContext({ - session, - }); -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ - -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an articifial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); - -/** - * Protected (authenticated) procedure - * - * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies - * the session is valid and guarantees `ctx.session.user` is not null. - * - * @see https://trpc.io/docs/procedures - */ -export const protectedProcedure = t.procedure - .use(timingMiddleware) - .use(({ ctx, next }) => { - if (!ctx.session || !ctx.session.user) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - return next({ - ctx: { - // infers the `session` as non-nullable - session: { ...ctx.session, user: ctx.session.user }, - }, - }); - }); diff --git a/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth.ts b/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth.ts deleted file mode 100644 index 87a6b0e2..00000000 --- a/packages/cli-old/template/extras/src/server/api/trpc-pages/with-auth.ts +++ /dev/null @@ -1,158 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ -import { initTRPC, TRPCError } from "@trpc/server"; -import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; -import { type Session } from "next-auth"; -import superjson from "superjson"; -import { ZodError } from "zod/v4"; - -import { getServerAuthSession } from "~/server/auth"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - */ - -interface CreateContextOptions { - session: Session | null; -} - -/** - * This helper generates the "internals" for a tRPC context. If you need to use it, you can export - * it from here. - * - * Examples of things you may need it for: - * - testing, so we don't have to mock Next.js' req/res - * - tRPC's `createSSGHelpers`, where we don't have req/res - * - * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts - */ -const createInnerTRPCContext = ({ session }: CreateContextOptions) => { - return { - session, - }; -}; - -/** - * This is the actual context you will use in your router. It will be used to process every request - * that goes through your tRPC endpoint. - * - * @see https://trpc.io/docs/context - */ -export const createTRPCContext = async ({ - req, - res, -}: CreateNextContextOptions) => { - // Get the session from the server using the getServerSession wrapper function - const session = await getServerAuthSession({ req, res }); - - return createInnerTRPCContext({ - session, - }); -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ - -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an articifial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); - -/** - * Protected (authenticated) procedure - * - * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies - * the session is valid and guarantees `ctx.session.user` is not null. - * - * @see https://trpc.io/docs/procedures - */ -export const protectedProcedure = t.procedure - .use(timingMiddleware) - .use(({ ctx, next }) => { - if (!ctx.session || !ctx.session.user) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - return next({ - ctx: { - // infers the `session` as non-nullable - session: { ...ctx.session, user: ctx.session.user }, - }, - }); - }); diff --git a/packages/cli-old/template/extras/src/server/api/trpc-pages/with-db.ts b/packages/cli-old/template/extras/src/server/api/trpc-pages/with-db.ts deleted file mode 100644 index a6e5bef7..00000000 --- a/packages/cli-old/template/extras/src/server/api/trpc-pages/with-db.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ -import { initTRPC } from "@trpc/server"; -import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; -import superjson from "superjson"; -import { ZodError } from "zod/v4"; - -import { db } from "~/server/db"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - */ - -type CreateContextOptions = Record; - -/** - * This helper generates the "internals" for a tRPC context. If you need to use it, you can export - * it from here. - * - * Examples of things you may need it for: - * - testing, so we don't have to mock Next.js' req/res - * - tRPC's `createSSGHelpers`, where we don't have req/res - * - * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts - */ -const createInnerTRPCContext = (_opts: CreateContextOptions) => { - return { - db, - }; -}; - -/** - * This is the actual context you will use in your router. It will be used to process every request - * that goes through your tRPC endpoint. - * - * @see https://trpc.io/docs/context - */ -export const createTRPCContext = (_opts: CreateNextContextOptions) => { - return createInnerTRPCContext({}); -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ - -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an articifial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/packages/cli-old/template/extras/src/server/data/users.ts b/packages/cli-old/template/extras/src/server/data/users.ts deleted file mode 100644 index fac1dc35..00000000 --- a/packages/cli-old/template/extras/src/server/data/users.ts +++ /dev/null @@ -1,23 +0,0 @@ -import "server-only"; - -import { fmAdapter } from "../auth"; -import { saltAndHashPassword } from "../password"; - -type UserSignUpInput = { - email: string; - password: string; -}; - -export async function userSignUp(input: UserSignUpInput) { - const passwordHash = await saltAndHashPassword(input.password); - - // create the user in our database - const user = await fmAdapter.typedClients.userWithPasswordHash.create({ - fieldData: { - email: input.email, - passwordHash, - }, - }); - - return user; -} diff --git a/packages/cli-old/template/extras/src/server/db/db-prisma-planetscale.ts b/packages/cli-old/template/extras/src/server/db/db-prisma-planetscale.ts deleted file mode 100644 index 52188938..00000000 --- a/packages/cli-old/template/extras/src/server/db/db-prisma-planetscale.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Client } from "@planetscale/database"; -import { PrismaPlanetScale } from "@prisma/adapter-planetscale"; -import { PrismaClient } from "@prisma/client"; - -import { env } from "~/env"; - -const psClient = new Client({ url: env.DATABASE_URL }); - -const createPrismaClient = () => - new PrismaClient({ - log: - env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], - adapter: new PrismaPlanetScale(psClient), - }); - -const globalForPrisma = globalThis as unknown as { - prisma: ReturnType | undefined; -}; - -export const db = globalForPrisma.prisma ?? createPrismaClient(); - -if (env.NODE_ENV !== "production") globalForPrisma.prisma = db; diff --git a/packages/cli-old/template/extras/src/server/db/db-prisma.ts b/packages/cli-old/template/extras/src/server/db/db-prisma.ts deleted file mode 100644 index 07dc0271..00000000 --- a/packages/cli-old/template/extras/src/server/db/db-prisma.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { PrismaClient } from "@prisma/client"; - -import { env } from "~/env"; - -const createPrismaClient = () => - new PrismaClient({ - log: - env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], - }); - -const globalForPrisma = globalThis as unknown as { - prisma: ReturnType | undefined; -}; - -export const db = globalForPrisma.prisma ?? createPrismaClient(); - -if (env.NODE_ENV !== "production") globalForPrisma.prisma = db; diff --git a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-mysql.ts b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-mysql.ts deleted file mode 100644 index 3542b7b8..00000000 --- a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-mysql.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { drizzle } from "drizzle-orm/mysql2"; -import { createPool, type Pool } from "mysql2/promise"; - -import { env } from "~/env"; -import * as schema from "./schema"; - -/** - * Cache the database connection in development. This avoids creating a new connection on every HMR - * update. - */ -const globalForDb = globalThis as unknown as { - conn: Pool | undefined; -}; - -const conn = globalForDb.conn ?? createPool({ uri: env.DATABASE_URL }); -if (env.NODE_ENV !== "production") globalForDb.conn = conn; - -export const db = drizzle(conn, { schema, mode: "default" }); diff --git a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-planetscale.ts b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-planetscale.ts deleted file mode 100644 index 4613a4c1..00000000 --- a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-planetscale.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Client } from "@planetscale/database"; -import { drizzle } from "drizzle-orm/planetscale-serverless"; - -import { env } from "~/env"; -import * as schema from "./schema"; - -export const db = drizzle(new Client({ url: env.DATABASE_URL }), { schema }); diff --git a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-postgres.ts b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-postgres.ts deleted file mode 100644 index 1287189a..00000000 --- a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-postgres.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { drizzle } from "drizzle-orm/postgres-js"; -import postgres from "postgres"; - -import { env } from "~/env"; -import * as schema from "./schema"; - -/** - * Cache the database connection in development. This avoids creating a new connection on every HMR - * update. - */ -const globalForDb = globalThis as unknown as { - conn: postgres.Sql | undefined; -}; - -const conn = globalForDb.conn ?? postgres(env.DATABASE_URL); -if (env.NODE_ENV !== "production") globalForDb.conn = conn; - -export const db = drizzle(conn, { schema }); diff --git a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-sqlite.ts b/packages/cli-old/template/extras/src/server/db/index-drizzle/with-sqlite.ts deleted file mode 100644 index ef1df14a..00000000 --- a/packages/cli-old/template/extras/src/server/db/index-drizzle/with-sqlite.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createClient, type Client } from "@libsql/client"; -import { drizzle } from "drizzle-orm/libsql"; - -import { env } from "~/env"; -import * as schema from "./schema"; - -/** - * Cache the database connection in development. This avoids creating a new connection on every HMR - * update. - */ -const globalForDb = globalThis as unknown as { - client: Client | undefined; -}; - -export const client = - globalForDb.client ?? createClient({ url: env.DATABASE_URL }); -if (env.NODE_ENV !== "production") globalForDb.client = client; - -export const db = drizzle(client, { schema }); diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-mysql.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-mysql.ts deleted file mode 100644 index bfb08079..00000000 --- a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-mysql.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Example model schema from the Drizzle docs -// https://orm.drizzle.team/docs/sql-schema-declaration - -import { sql } from "drizzle-orm"; -import { - bigint, - index, - mysqlTableCreator, - timestamp, - varchar, -} from "drizzle-orm/mysql-core"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = mysqlTableCreator((name) => `project1_${name}`); - -export const posts = createTable( - "post", - { - id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), - name: varchar("name", { length: 256 }), - createdAt: timestamp("created_at") - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), - }, - (example) => ({ - nameIndex: index("name_idx").on(example.name), - }) -); diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-planetscale.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-planetscale.ts deleted file mode 100644 index bfb08079..00000000 --- a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-planetscale.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Example model schema from the Drizzle docs -// https://orm.drizzle.team/docs/sql-schema-declaration - -import { sql } from "drizzle-orm"; -import { - bigint, - index, - mysqlTableCreator, - timestamp, - varchar, -} from "drizzle-orm/mysql-core"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = mysqlTableCreator((name) => `project1_${name}`); - -export const posts = createTable( - "post", - { - id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), - name: varchar("name", { length: 256 }), - createdAt: timestamp("created_at") - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), - }, - (example) => ({ - nameIndex: index("name_idx").on(example.name), - }) -); diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-postgres.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-postgres.ts deleted file mode 100644 index 8e6f2f99..00000000 --- a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-postgres.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Example model schema from the Drizzle docs -// https://orm.drizzle.team/docs/sql-schema-declaration - -import { sql } from "drizzle-orm"; -import { - index, - pgTableCreator, - serial, - timestamp, - varchar, -} from "drizzle-orm/pg-core"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = pgTableCreator((name) => `project1_${name}`); - -export const posts = createTable( - "post", - { - id: serial("id").primaryKey(), - name: varchar("name", { length: 256 }), - createdAt: timestamp("created_at", { withTimezone: true }) - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( - () => new Date() - ), - }, - (example) => ({ - nameIndex: index("name_idx").on(example.name), - }) -); diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-sqlite.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-sqlite.ts deleted file mode 100644 index cc74c86a..00000000 --- a/packages/cli-old/template/extras/src/server/db/schema-drizzle/base-sqlite.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Example model schema from the Drizzle docs -// https://orm.drizzle.team/docs/sql-schema-declaration - -import { sql } from "drizzle-orm"; -import { index, int, sqliteTableCreator, text } from "drizzle-orm/sqlite-core"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = sqliteTableCreator((name) => `project1_${name}`); - -export const posts = createTable( - "post", - { - id: int("id", { mode: "number" }).primaryKey({ autoIncrement: true }), - name: text("name", { length: 256 }), - createdAt: int("created_at", { mode: "timestamp" }) - .default(sql`(unixepoch())`) - .notNull(), - updatedAt: int("updated_at", { mode: "timestamp" }).$onUpdate( - () => new Date() - ), - }, - (example) => ({ - nameIndex: index("name_idx").on(example.name), - }) -); diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts deleted file mode 100644 index 96e9a85c..00000000 --- a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { relations, sql } from "drizzle-orm"; -import { - bigint, - index, - int, - mysqlTableCreator, - primaryKey, - text, - timestamp, - varchar, -} from "drizzle-orm/mysql-core"; -import { type AdapterAccount } from "next-auth/adapters"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = mysqlTableCreator((name) => `project1_${name}`); - -export const posts = createTable( - "post", - { - id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), - name: varchar("name", { length: 256 }), - createdById: varchar("created_by", { length: 255 }) - .notNull() - .references(() => users.id), - createdAt: timestamp("created_at") - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), - }, - (example) => ({ - createdByIdIdx: index("created_by_idx").on(example.createdById), - nameIndex: index("name_idx").on(example.name), - }) -); - -export const users = createTable("user", { - id: varchar("id", { length: 255 }) - .notNull() - .primaryKey() - .$defaultFn(() => crypto.randomUUID()), - name: varchar("name", { length: 255 }), - email: varchar("email", { length: 255 }).notNull(), - emailVerified: timestamp("email_verified", { - mode: "date", - fsp: 3, - }).default(sql`CURRENT_TIMESTAMP(3)`), - image: varchar("image", { length: 255 }), -}); - -export const usersRelations = relations(users, ({ many }) => ({ - accounts: many(accounts), - sessions: many(sessions), -})); - -export const accounts = createTable( - "account", - { - userId: varchar("user_id", { length: 255 }) - .notNull() - .references(() => users.id), - type: varchar("type", { length: 255 }) - .$type() - .notNull(), - provider: varchar("provider", { length: 255 }).notNull(), - providerAccountId: varchar("provider_account_id", { - length: 255, - }).notNull(), - refresh_token: text("refresh_token"), - access_token: text("access_token"), - expires_at: int("expires_at"), - token_type: varchar("token_type", { length: 255 }), - scope: varchar("scope", { length: 255 }), - id_token: text("id_token"), - session_state: varchar("session_state", { length: 255 }), - }, - (account) => ({ - compoundKey: primaryKey({ - columns: [account.provider, account.providerAccountId], - }), - userIdIdx: index("account_user_id_idx").on(account.userId), - }) -); - -export const accountsRelations = relations(accounts, ({ one }) => ({ - user: one(users, { fields: [accounts.userId], references: [users.id] }), -})); - -export const sessions = createTable( - "session", - { - sessionToken: varchar("session_token", { length: 255 }) - .notNull() - .primaryKey(), - userId: varchar("user_id", { length: 255 }) - .notNull() - .references(() => users.id), - expires: timestamp("expires", { mode: "date" }).notNull(), - }, - (session) => ({ - userIdIdx: index("session_user_id_idx").on(session.userId), - }) -); - -export const sessionsRelations = relations(sessions, ({ one }) => ({ - user: one(users, { fields: [sessions.userId], references: [users.id] }), -})); - -export const verificationTokens = createTable( - "verification_token", - { - identifier: varchar("identifier", { length: 255 }).notNull(), - token: varchar("token", { length: 255 }).notNull(), - expires: timestamp("expires", { mode: "date" }).notNull(), - }, - (vt) => ({ - compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), - }) -); diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts deleted file mode 100644 index a0b1d72f..00000000 --- a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { relations, sql } from "drizzle-orm"; -import { - bigint, - index, - int, - mysqlTableCreator, - primaryKey, - text, - timestamp, - varchar, -} from "drizzle-orm/mysql-core"; -import { type AdapterAccount } from "next-auth/adapters"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = mysqlTableCreator((name) => `project1_${name}`); - -export const posts = createTable( - "post", - { - id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), - name: varchar("name", { length: 256 }), - createdById: varchar("created_by", { length: 255 }).notNull(), - createdAt: timestamp("created_at") - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), - }, - (example) => ({ - createdByIdIdx: index("created_by_idx").on(example.createdById), - nameIndex: index("name_idx").on(example.name), - }) -); - -export const users = createTable("user", { - id: varchar("id", { length: 255 }) - .notNull() - .primaryKey() - .$defaultFn(() => crypto.randomUUID()), - name: varchar("name", { length: 255 }), - email: varchar("email", { length: 255 }).notNull(), - emailVerified: timestamp("email_verified", { - mode: "date", - fsp: 3, - }).default(sql`CURRENT_TIMESTAMP(3)`), - image: varchar("image", { length: 255 }), -}); - -export const usersRelations = relations(users, ({ many }) => ({ - accounts: many(accounts), - sessions: many(sessions), -})); - -export const accounts = createTable( - "account", - { - userId: varchar("user_id", { length: 255 }).notNull(), - type: varchar("type", { length: 255 }) - .$type() - .notNull(), - provider: varchar("provider", { length: 255 }).notNull(), - providerAccountId: varchar("provider_account_id", { - length: 255, - }).notNull(), - refresh_token: text("refresh_token"), - access_token: text("access_token"), - expires_at: int("expires_at"), - token_type: varchar("token_type", { length: 255 }), - scope: varchar("scope", { length: 255 }), - id_token: text("id_token"), - session_state: varchar("session_state", { length: 255 }), - }, - (account) => ({ - compoundKey: primaryKey({ - columns: [account.provider, account.providerAccountId], - }), - userIdIdx: index("accounts_user_id_idx").on(account.userId), - }) -); - -export const accountsRelations = relations(accounts, ({ one }) => ({ - user: one(users, { fields: [accounts.userId], references: [users.id] }), -})); - -export const sessions = createTable( - "session", - { - sessionToken: varchar("session_token", { length: 255 }) - .notNull() - .primaryKey(), - userId: varchar("user_id", { length: 255 }).notNull(), - expires: timestamp("expires", { mode: "date" }).notNull(), - }, - (session) => ({ - userIdIdx: index("session_user_id_idx").on(session.userId), - }) -); - -export const sessionsRelations = relations(sessions, ({ one }) => ({ - user: one(users, { fields: [sessions.userId], references: [users.id] }), -})); - -export const verificationTokens = createTable( - "verification_token", - { - identifier: varchar("identifier", { length: 255 }).notNull(), - token: varchar("token", { length: 255 }).notNull(), - expires: timestamp("expires", { mode: "date" }).notNull(), - }, - (vt) => ({ - compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), - }) -); diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts deleted file mode 100644 index 5ce3f9c2..00000000 --- a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { relations, sql } from "drizzle-orm"; -import { - index, - integer, - pgTableCreator, - primaryKey, - serial, - text, - timestamp, - varchar, -} from "drizzle-orm/pg-core"; -import { type AdapterAccount } from "next-auth/adapters"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = pgTableCreator((name) => `project1_${name}`); - -export const posts = createTable( - "post", - { - id: serial("id").primaryKey(), - name: varchar("name", { length: 256 }), - createdById: varchar("created_by", { length: 255 }) - .notNull() - .references(() => users.id), - createdAt: timestamp("created_at", { withTimezone: true }) - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( - () => new Date() - ), - }, - (example) => ({ - createdByIdIdx: index("created_by_idx").on(example.createdById), - nameIndex: index("name_idx").on(example.name), - }) -); - -export const users = createTable("user", { - id: varchar("id", { length: 255 }) - .notNull() - .primaryKey() - .$defaultFn(() => crypto.randomUUID()), - name: varchar("name", { length: 255 }), - email: varchar("email", { length: 255 }).notNull(), - emailVerified: timestamp("email_verified", { - mode: "date", - withTimezone: true, - }).default(sql`CURRENT_TIMESTAMP`), - image: varchar("image", { length: 255 }), -}); - -export const usersRelations = relations(users, ({ many }) => ({ - accounts: many(accounts), -})); - -export const accounts = createTable( - "account", - { - userId: varchar("user_id", { length: 255 }) - .notNull() - .references(() => users.id), - type: varchar("type", { length: 255 }) - .$type() - .notNull(), - provider: varchar("provider", { length: 255 }).notNull(), - providerAccountId: varchar("provider_account_id", { - length: 255, - }).notNull(), - refresh_token: text("refresh_token"), - access_token: text("access_token"), - expires_at: integer("expires_at"), - token_type: varchar("token_type", { length: 255 }), - scope: varchar("scope", { length: 255 }), - id_token: text("id_token"), - session_state: varchar("session_state", { length: 255 }), - }, - (account) => ({ - compoundKey: primaryKey({ - columns: [account.provider, account.providerAccountId], - }), - userIdIdx: index("account_user_id_idx").on(account.userId), - }) -); - -export const accountsRelations = relations(accounts, ({ one }) => ({ - user: one(users, { fields: [accounts.userId], references: [users.id] }), -})); - -export const sessions = createTable( - "session", - { - sessionToken: varchar("session_token", { length: 255 }) - .notNull() - .primaryKey(), - userId: varchar("user_id", { length: 255 }) - .notNull() - .references(() => users.id), - expires: timestamp("expires", { - mode: "date", - withTimezone: true, - }).notNull(), - }, - (session) => ({ - userIdIdx: index("session_user_id_idx").on(session.userId), - }) -); - -export const sessionsRelations = relations(sessions, ({ one }) => ({ - user: one(users, { fields: [sessions.userId], references: [users.id] }), -})); - -export const verificationTokens = createTable( - "verification_token", - { - identifier: varchar("identifier", { length: 255 }).notNull(), - token: varchar("token", { length: 255 }).notNull(), - expires: timestamp("expires", { - mode: "date", - withTimezone: true, - }).notNull(), - }, - (vt) => ({ - compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), - }) -); diff --git a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts b/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts deleted file mode 100644 index 12ee2901..00000000 --- a/packages/cli-old/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { relations, sql } from "drizzle-orm"; -import { - index, - int, - primaryKey, - sqliteTableCreator, - text, -} from "drizzle-orm/sqlite-core"; -import { type AdapterAccount } from "next-auth/adapters"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = sqliteTableCreator((name) => `project1_${name}`); - -export const posts = createTable( - "post", - { - id: int("id", { mode: "number" }).primaryKey({ autoIncrement: true }), - name: text("name", { length: 256 }), - createdById: text("created_by", { length: 255 }) - .notNull() - .references(() => users.id), - createdAt: int("created_at", { mode: "timestamp" }) - .default(sql`(unixepoch())`) - .notNull(), - updatedAt: int("updatedAt", { mode: "timestamp" }).$onUpdate( - () => new Date() - ), - }, - (example) => ({ - createdByIdIdx: index("created_by_idx").on(example.createdById), - nameIndex: index("name_idx").on(example.name), - }) -); - -export const users = createTable("user", { - id: text("id", { length: 255 }) - .notNull() - .primaryKey() - .$defaultFn(() => crypto.randomUUID()), - name: text("name", { length: 255 }), - email: text("email", { length: 255 }).notNull(), - emailVerified: int("email_verified", { - mode: "timestamp", - }).default(sql`(unixepoch())`), - image: text("image", { length: 255 }), -}); - -export const usersRelations = relations(users, ({ many }) => ({ - accounts: many(accounts), -})); - -export const accounts = createTable( - "account", - { - userId: text("user_id", { length: 255 }) - .notNull() - .references(() => users.id), - type: text("type", { length: 255 }) - .$type() - .notNull(), - provider: text("provider", { length: 255 }).notNull(), - providerAccountId: text("provider_account_id", { length: 255 }).notNull(), - refresh_token: text("refresh_token"), - access_token: text("access_token"), - expires_at: int("expires_at"), - token_type: text("token_type", { length: 255 }), - scope: text("scope", { length: 255 }), - id_token: text("id_token"), - session_state: text("session_state", { length: 255 }), - }, - (account) => ({ - compoundKey: primaryKey({ - columns: [account.provider, account.providerAccountId], - }), - userIdIdx: index("account_user_id_idx").on(account.userId), - }) -); - -export const accountsRelations = relations(accounts, ({ one }) => ({ - user: one(users, { fields: [accounts.userId], references: [users.id] }), -})); - -export const sessions = createTable( - "session", - { - sessionToken: text("session_token", { length: 255 }).notNull().primaryKey(), - userId: text("userId", { length: 255 }) - .notNull() - .references(() => users.id), - expires: int("expires", { mode: "timestamp" }).notNull(), - }, - (session) => ({ - userIdIdx: index("session_userId_idx").on(session.userId), - }) -); - -export const sessionsRelations = relations(sessions, ({ one }) => ({ - user: one(users, { fields: [sessions.userId], references: [users.id] }), -})); - -export const verificationTokens = createTable( - "verification_token", - { - identifier: text("identifier", { length: 255 }).notNull(), - token: text("token", { length: 255 }).notNull(), - expires: int("expires", { mode: "timestamp" }).notNull(), - }, - (vt) => ({ - compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), - }) -); diff --git a/packages/cli-old/template/extras/src/server/next-auth/base.ts b/packages/cli-old/template/extras/src/server/next-auth/base.ts deleted file mode 100644 index ac29d61f..00000000 --- a/packages/cli-old/template/extras/src/server/next-auth/base.ts +++ /dev/null @@ -1,111 +0,0 @@ - - - - -import { env } from "@/config/env"; -import { OttoAdapter } from "@proofkit/fmdapi"; -import NextAuth, { type DefaultSession } from "next-auth"; -import { FilemakerAdapter } from "next-auth-adapter-filemaker"; -import { type Provider } from "next-auth/providers"; -import Credentials from "next-auth/providers/credentials"; -import { z } from "zod/v4"; - -import { verifyPassword } from "./password"; - -export const fmAdapter = FilemakerAdapter({ - adapter: new OttoAdapter({ - auth: { apiKey: env.OTTO_API_KEY }, - db: env.FM_DATABASE, - server: env.FM_SERVER, - }), -}); - -/** - * Module augmentation for `next-auth` types. Alldows us to add custom properties to the `session` - * object and keep type safety. - * - * @see https://next-auth.js.org/getting-started/typescript#module-augmentation - */ -declare module "next-auth" { - interface Session extends DefaultSession { - user: { - id: string; - // ...other properties - // role: UserRole; - } & DefaultSession["user"]; - } - - // interface User { - // // ...other properties - // // role: UserRole; - // } -} - -const signInSchema = z.object({ - email: z.string().email(), - password: z.string(), -}); - -const providers: Provider[] = [ - Credentials({ - credentials: { - email: { label: "Email", type: "email" }, - password: { label: "Password", type: "password" }, - }, - authorize: async (credentials) => { - const parsed = signInSchema.safeParse(credentials); - if (!parsed.success) { - return null; - } - - const { email, password } = parsed.data; - - try { - // logic to verify if the user exists with the password hash - const userResponse = - await fmAdapter.typedClients.userWithPasswordHash.findOne({ - query: { email: `==${email.replace("@", "\\@")}` }, - }); - const { passwordHash, ...userData } = userResponse.data.fieldData; - const isValid = await verifyPassword(password, passwordHash); - if (!isValid) return null; - - return userData; - } catch (error) { - console.log("error", error); - throw new Error("User not found."); - } - }, - }), -]; - -export const providerMap = providers - .map((provider) => { - if (typeof provider === "function") { - const providerData = provider(); - return { id: providerData.id, name: providerData.name }; - } else { - return { id: provider.id, name: provider.name }; - } - }) - .filter((provider) => provider.id !== "credentials"); - -export const { auth, handlers, signIn, signOut } = NextAuth({ - pages: { - signIn: "/auth/signin", - newUser: "/auth/signup", - error: "/auth/signin", - }, - callbacks: { - session: ({ session, token }) => ({ - ...session, - user: { - ...session.user, - id: token.sub, - }, - }), - }, - adapter: fmAdapter.Adapter, - session: { strategy: "jwt" }, - providers, -}); diff --git a/packages/cli-old/template/extras/src/server/next-auth/password.ts b/packages/cli-old/template/extras/src/server/next-auth/password.ts deleted file mode 100644 index a82f34c6..00000000 --- a/packages/cli-old/template/extras/src/server/next-auth/password.ts +++ /dev/null @@ -1,13 +0,0 @@ -export async function saltAndHashPassword(password: string): Promise { - const bcrypt = await import("bcrypt"); - const saltRounds = 12; - return bcrypt.hash(password, saltRounds); -} - -export async function verifyPassword( - plainTextPassword: string, - hashedPassword: string -): Promise { - const bcrypt = await import("bcrypt"); - return bcrypt.compare(plainTextPassword, hashedPassword); -} diff --git a/packages/cli-old/template/extras/src/server/next-auth/with-drizzle.ts b/packages/cli-old/template/extras/src/server/next-auth/with-drizzle.ts deleted file mode 100644 index 6e9281d1..00000000 --- a/packages/cli-old/template/extras/src/server/next-auth/with-drizzle.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { DrizzleAdapter } from "@auth/drizzle-adapter"; -import { - getServerSession, - type DefaultSession, - type NextAuthOptions, -} from "next-auth"; -import { type Adapter } from "next-auth/adapters"; -import DiscordProvider from "next-auth/providers/discord"; - -import { env } from "~/env"; -import { db } from "~/server/db"; -import { - accounts, - sessions, - users, - verificationTokens, -} from "~/server/db/schema"; - -/** - * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` - * object and keep type safety. - * - * @see https://next-auth.js.org/getting-started/typescript#module-augmentation - */ -declare module "next-auth" { - interface Session extends DefaultSession { - user: { - id: string; - // ...other properties - // role: UserRole; - } & DefaultSession["user"]; - } - - // interface User { - // // ...other properties - // // role: UserRole; - // } -} - -/** - * Options for NextAuth.js used to configure adapters, providers, callbacks, etc. - * - * @see https://next-auth.js.org/configuration/options - */ -export const authOptions: NextAuthOptions = { - callbacks: { - session: ({ session, user }) => ({ - ...session, - user: { - ...session.user, - id: user.id, - }, - }), - }, - adapter: DrizzleAdapter(db, { - usersTable: users, - accountsTable: accounts, - sessionsTable: sessions, - verificationTokensTable: verificationTokens, - }) as Adapter, - providers: [ - DiscordProvider({ - clientId: env.DISCORD_CLIENT_ID, - clientSecret: env.DISCORD_CLIENT_SECRET, - }), - /** - * ...add more providers here. - * - * Most other providers require a bit more work than the Discord provider. For example, the - * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account - * model. Refer to the NextAuth.js docs for the provider you want to use. Example: - * - * @see https://next-auth.js.org/providers/github - */ - ], -}; - -/** - * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file. - * - * @see https://next-auth.js.org/configuration/nextjs - */ -export const getServerAuthSession = () => getServerSession(authOptions); diff --git a/packages/cli-old/template/extras/src/server/next-auth/with-prisma.ts b/packages/cli-old/template/extras/src/server/next-auth/with-prisma.ts deleted file mode 100644 index 117984c9..00000000 --- a/packages/cli-old/template/extras/src/server/next-auth/with-prisma.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { PrismaAdapter } from "@auth/prisma-adapter"; -import { - getServerSession, - type DefaultSession, - type NextAuthOptions, -} from "next-auth"; -import { type Adapter } from "next-auth/adapters"; -import DiscordProvider from "next-auth/providers/discord"; - -import { env } from "~/env"; -import { db } from "~/server/db"; - -/** - * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` - * object and keep type safety. - * - * @see https://next-auth.js.org/getting-started/typescript#module-augmentation - */ -declare module "next-auth" { - interface Session extends DefaultSession { - user: { - id: string; - // ...other properties - // role: UserRole; - } & DefaultSession["user"]; - } - - // interface User { - // // ...other properties - // // role: UserRole; - // } -} - -/** - * Options for NextAuth.js used to configure adapters, providers, callbacks, etc. - * - * @see https://next-auth.js.org/configuration/options - */ -export const authOptions: NextAuthOptions = { - callbacks: { - session: ({ session, user }) => ({ - ...session, - user: { - ...session.user, - id: user.id, - }, - }), - }, - adapter: PrismaAdapter(db) as Adapter, - providers: [ - DiscordProvider({ - clientId: env.DISCORD_CLIENT_ID, - clientSecret: env.DISCORD_CLIENT_SECRET, - }), - /** - * ...add more providers here. - * - * Most other providers require a bit more work than the Discord provider. For example, the - * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account - * model. Refer to the NextAuth.js docs for the provider you want to use. Example: - * - * @see https://next-auth.js.org/providers/github - */ - ], -}; - -/** - * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file. - * - * @see https://next-auth.js.org/configuration/nextjs - */ -export const getServerAuthSession = () => getServerSession(authOptions); diff --git a/packages/cli-old/template/extras/src/trpc/query-client.ts b/packages/cli-old/template/extras/src/trpc/query-client.ts deleted file mode 100644 index bda64397..00000000 --- a/packages/cli-old/template/extras/src/trpc/query-client.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - defaultShouldDehydrateQuery, - QueryClient, -} from "@tanstack/react-query"; -import SuperJSON from "superjson"; - -export const createQueryClient = () => - new QueryClient({ - defaultOptions: { - queries: { - // With SSR, we usually want to set some default staleTime - // above 0 to avoid refetching immediately on the client - staleTime: 30 * 1000, - }, - dehydrate: { - serializeData: SuperJSON.serialize, - shouldDehydrateQuery: (query) => - defaultShouldDehydrateQuery(query) || - query.state.status === "pending", - }, - hydrate: { - deserializeData: SuperJSON.deserialize, - }, - }, - }); diff --git a/packages/cli-old/template/extras/src/trpc/react.tsx b/packages/cli-old/template/extras/src/trpc/react.tsx deleted file mode 100644 index 8c0521a7..00000000 --- a/packages/cli-old/template/extras/src/trpc/react.tsx +++ /dev/null @@ -1,76 +0,0 @@ -"use client"; - -import { QueryClientProvider, type QueryClient } from "@tanstack/react-query"; -import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client"; -import { createTRPCReact } from "@trpc/react-query"; -import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server"; -import { useState } from "react"; -import SuperJSON from "superjson"; - -import { type AppRouter } from "~/server/api/root"; -import { createQueryClient } from "./query-client"; - -let clientQueryClientSingleton: QueryClient | undefined = undefined; -const getQueryClient = () => { - if (typeof window === "undefined") { - // Server: always make a new query client - return createQueryClient(); - } - // Browser: use singleton pattern to keep the same query client - return (clientQueryClientSingleton ??= createQueryClient()); -}; - -export const api = createTRPCReact(); - -/** - * Inference helper for inputs. - * - * @example type HelloInput = RouterInputs['example']['hello'] - */ -export type RouterInputs = inferRouterInputs; - -/** - * Inference helper for outputs. - * - * @example type HelloOutput = RouterOutputs['example']['hello'] - */ -export type RouterOutputs = inferRouterOutputs; - -export function TRPCReactProvider(props: { children: React.ReactNode }) { - const queryClient = getQueryClient(); - - const [trpcClient] = useState(() => - api.createClient({ - links: [ - loggerLink({ - enabled: (op) => - process.env.NODE_ENV === "development" || - (op.direction === "down" && op.result instanceof Error), - }), - unstable_httpBatchStreamLink({ - transformer: SuperJSON, - url: getBaseUrl() + "/api/trpc", - headers: () => { - const headers = new Headers(); - headers.set("x-trpc-source", "nextjs-react"); - return headers; - }, - }), - ], - }) - ); - - return ( - - - {props.children} - - - ); -} - -function getBaseUrl() { - if (typeof window !== "undefined") return window.location.origin; - if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; - return `http://localhost:${process.env.PORT ?? 3000}`; -} diff --git a/packages/cli-old/template/extras/src/trpc/server.ts b/packages/cli-old/template/extras/src/trpc/server.ts deleted file mode 100644 index 59300a63..00000000 --- a/packages/cli-old/template/extras/src/trpc/server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import "server-only"; - -import { createHydrationHelpers } from "@trpc/react-query/rsc"; -import { headers } from "next/headers"; -import { cache } from "react"; - -import { createCaller, type AppRouter } from "~/server/api/root"; -import { createTRPCContext } from "~/server/api/trpc"; -import { createQueryClient } from "./query-client"; - -/** - * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when - * handling a tRPC call from a React Server Component. - */ -const createContext = cache(() => { - const heads = new Headers(headers()); - heads.set("x-trpc-source", "rsc"); - - return createTRPCContext({ - headers: heads, - }); -}); - -const getQueryClient = cache(createQueryClient); -const caller = createCaller(createContext); - -export const { trpc: api, HydrateClient } = createHydrationHelpers( - caller, - getQueryClient -); diff --git a/packages/cli-old/template/extras/src/utils/api.ts b/packages/cli-old/template/extras/src/utils/api.ts deleted file mode 100644 index 0f03d307..00000000 --- a/packages/cli-old/template/extras/src/utils/api.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * This is the client-side entrypoint for your tRPC API. It is used to create the `api` object which - * contains the Next.js App-wrapper, as well as your type-safe React Query hooks. - * - * We also create a few inference helpers for input and output types. - */ -import { httpBatchLink, loggerLink } from "@trpc/client"; -import { createTRPCNext } from "@trpc/next"; -import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server"; -import superjson from "superjson"; - -import { type AppRouter } from "~/server/api/root"; - -const getBaseUrl = () => { - if (typeof window !== "undefined") return ""; // browser should use relative url - if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url - return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost -}; - -/** A set of type-safe react-query hooks for your tRPC API. */ -export const api = createTRPCNext({ - config() { - return { - /** - * Links used to determine request flow from client to server. - * - * @see https://trpc.io/docs/links - */ - links: [ - loggerLink({ - enabled: (opts) => - process.env.NODE_ENV === "development" || - (opts.direction === "down" && opts.result instanceof Error), - }), - httpBatchLink({ - /** - * Transformer used for data de-serialization from the server. - * - * @see https://trpc.io/docs/data-transformers - */ - transformer: superjson, - url: `${getBaseUrl()}/api/trpc`, - }), - ], - }; - }, - /** - * Whether tRPC should await queries when server rendering pages. - * - * @see https://trpc.io/docs/nextjs#ssr-boolean-default-false - */ - ssr: false, - transformer: superjson, -}); - -/** - * Inference helper for inputs. - * - * @example type HelloInput = RouterInputs['example']['hello'] - */ -export type RouterInputs = inferRouterInputs; - -/** - * Inference helper for outputs. - * - * @example type HelloOutput = RouterOutputs['example']['hello'] - */ -export type RouterOutputs = inferRouterOutputs; diff --git a/packages/cli-old/template/extras/start-database/mysql.sh b/packages/cli-old/template/extras/start-database/mysql.sh deleted file mode 100755 index 268df5cc..00000000 --- a/packages/cli-old/template/extras/start-database/mysql.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -# Use this script to start a docker container for a local development database - -# TO RUN ON WINDOWS: -# 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install -# 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/ -# 3. Open WSL - `wsl` -# 4. Run this script - `./start-database.sh` - -# On Linux and macOS you can run this script directly - `./start-database.sh` - -DB_CONTAINER_NAME="project1-mysql" - -if ! [ -x "$(command -v docker)" ]; then - echo -e "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/" - exit 1 -fi - -if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then - echo "Database container '$DB_CONTAINER_NAME' already running" - exit 0 -fi - -if [ "$(docker ps -q -a -f name=$DB_CONTAINER_NAME)" ]; then - docker start "$DB_CONTAINER_NAME" - echo "Existing database container '$DB_CONTAINER_NAME' started" - exit 0 -fi - -# import env variables from .env -set -a -source .env - -DB_PASSWORD=$(echo "$DATABASE_URL" | awk -F':' '{print $3}' | awk -F'@' '{print $1}') -DB_PORT=$(echo "$DATABASE_URL" | awk -F':' '{print $4}' | awk -F'\/' '{print $1}') - -if [ "$DB_PASSWORD" == "password" ]; then - echo "You are using the default database password" - read -p "Should we generate a random password for you? [y/N]: " -r REPLY - if ! [[ $REPLY =~ ^[Yy]$ ]]; then - echo "Please change the default password in the .env file and try again" - exit 1 - fi - # Generate a random URL-safe password - DB_PASSWORD=$(openssl rand -base64 12 | tr '+/' '-_') - sed -i -e "s#:password@#:$DB_PASSWORD@#" .env -fi - -docker run -d \ - --name $DB_CONTAINER_NAME \ - -e MYSQL_ROOT_PASSWORD="$DB_PASSWORD" \ - -e MYSQL_DATABASE=project1 \ - -p "$DB_PORT":3306 \ - docker.io/mysql && echo "Database container '$DB_CONTAINER_NAME' was successfully created" diff --git a/packages/cli-old/template/extras/start-database/postgres.sh b/packages/cli-old/template/extras/start-database/postgres.sh deleted file mode 100755 index 11fb2042..00000000 --- a/packages/cli-old/template/extras/start-database/postgres.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash -# Use this script to start a docker container for a local development database - -# TO RUN ON WINDOWS: -# 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install -# 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/ -# 3. Open WSL - `wsl` -# 4. Run this script - `./start-database.sh` - -# On Linux and macOS you can run this script directly - `./start-database.sh` - -DB_CONTAINER_NAME="project1-postgres" - -if ! [ -x "$(command -v docker)" ]; then - echo -e "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/" - exit 1 -fi - -if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then - echo "Database container '$DB_CONTAINER_NAME' already running" - exit 0 -fi - -if [ "$(docker ps -q -a -f name=$DB_CONTAINER_NAME)" ]; then - docker start "$DB_CONTAINER_NAME" - echo "Existing database container '$DB_CONTAINER_NAME' started" - exit 0 -fi - -# import env variables from .env -set -a -source .env - -DB_PASSWORD=$(echo "$DATABASE_URL" | awk -F':' '{print $3}' | awk -F'@' '{print $1}') -DB_PORT=$(echo "$DATABASE_URL" | awk -F':' '{print $4}' | awk -F'\/' '{print $1}') - -if [ "$DB_PASSWORD" = "password" ]; then - echo "You are using the default database password" - read -p "Should we generate a random password for you? [y/N]: " -r REPLY - if ! [[ $REPLY =~ ^[Yy]$ ]]; then - echo "Please change the default password in the .env file and try again" - exit 1 - fi - # Generate a random URL-safe password - DB_PASSWORD=$(openssl rand -base64 12 | tr '+/' '-_') - sed -i -e "s#:password@#:$DB_PASSWORD@#" .env -fi - -docker run -d \ - --name $DB_CONTAINER_NAME \ - -e POSTGRES_USER="postgres" \ - -e POSTGRES_PASSWORD="$DB_PASSWORD" \ - -e POSTGRES_DB=project1 \ - -p "$DB_PORT":5432 \ - docker.io/postgres && echo "Database container '$DB_CONTAINER_NAME' was successfully created" diff --git a/packages/cli-old/template/nextjs-mantine/README.md b/packages/cli-old/template/nextjs-mantine/README.md deleted file mode 100644 index 15794a3d..00000000 --- a/packages/cli-old/template/nextjs-mantine/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# ProofKit NextJS Template - -This is a [NextJS](https://nextjs.org/) project bootstrapped with `@proofkit/cli`. Learn more at [proofkit.proof.sh](https://proofkit.proof.sh) - -## What's next? How do I make an app with this? - -While this template is designed to be a minimal starting point, the proofkit CLI will guide you through adding additional features and pages. - -To add new things to your project, simply run the `proofkit` script from the project's root directory. - -e.g. `npm run proofkit` or `pnpm proofkit` etc. - -For more information, see the full [ProofKit documentation](https://proofkit.proof.sh). - -## Project Structure - -ProofKit projects have an opinionated structure to help you get started and some conventions must be maintained to ensure that the CLI can properly inject new features and components. - -The `src` directory is the home for your application code. It is used for most things except for configuration and is organized as follows: - -- `app` - NextJS app router, where your pages and routes are defined -- `components` - Shared components used throughout the app -- `server` - Code that connects to backend databases and services that should not be exposed in the browser - -Anytime you see an `internal` folder, you should not modify any files inside. These files are maintained exclusively by the ProofKit CLI and changes to them may be overwritten. - -Anytime you see a componet file that begins with `slot-`, you _may_ modify the content, but do not rename, remove, or move them. These are desigend to be customized, but are still used by the CLI to inject additional content. If a slot is not needed by your app, you can have the compoment return `null` or an empty fragment: `<>` diff --git a/packages/cli-old/template/nextjs-mantine/_gitignore b/packages/cli-old/template/nextjs-mantine/_gitignore deleted file mode 100644 index 00bba9bb..00000000 --- a/packages/cli-old/template/nextjs-mantine/_gitignore +++ /dev/null @@ -1,37 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local -.env - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/packages/cli-old/template/nextjs-mantine/components.json b/packages/cli-old/template/nextjs-mantine/components.json deleted file mode 100644 index 0d27c449..00000000 --- a/packages/cli-old/template/nextjs-mantine/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/config/theme/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "tw:" - }, - "aliases": { - "components": "@/components", - "utils": "@/utils/styles", - "ui": "@/components/ui", - "lib": "@/utils", - "hooks": "@/utils/hooks" - }, - "iconLibrary": "lucide" -} diff --git a/packages/cli-old/template/nextjs-mantine/next.config.ts b/packages/cli-old/template/nextjs-mantine/next.config.ts deleted file mode 100644 index 9555317e..00000000 --- a/packages/cli-old/template/nextjs-mantine/next.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type NextConfig } from "next"; - -// Import env here to validate during build. -import "./src/config/env"; - -const nextConfig: NextConfig = { - experimental: { - optimizePackageImports: ["@mantine/core", "@mantine/hooks"], - }, -}; - -export default nextConfig; diff --git a/packages/cli-old/template/nextjs-mantine/package.json b/packages/cli-old/template/nextjs-mantine/package.json deleted file mode 100644 index 31fad730..00000000 --- a/packages/cli-old/template/nextjs-mantine/package.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "template", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev --turbopack", - "build": "next build", - "start": "next start", - "lint": "biome check", - "format": "biome format --write", - "proofkit": "proofkit", - "typegen": "proofkit typegen", - "deploy": "proofkit deploy" - }, - "dependencies": { - "@hookform/resolvers": "^5.1.1", - "@next-safe-action/adapter-react-hook-form": "^2.0.0", - "next-safe-action": "^8.0.4", - "react-hook-form": "^7.54.2", - "@tabler/icons-react": "^3.30.0", - "@mantine/core": "^7.17.0", - "@mantine/dates": "^7.17.0", - "@mantine/hooks": "^7.17.0", - "@mantine/modals": "^7.17.0", - "@mantine/notifications": "^7.17.0", - "mantine-react-table": "2.0.0-beta.9", - "@t3-oss/env-nextjs": "^0.12.0", - "dayjs": "^1.11.13", - "next": "^15.2.7", - "react": "19.0.0", - "react-dom": "19.0.0", - "zod": "^3.24.2" - }, - "devDependencies": { - "@types/node": "^20", - "@types/react": "npm:types-react@19.0.12", - "@types/react-dom": "npm:types-react-dom@19.0.4", - "@biomejs/biome": "2.3.11", - "postcss": "^8.4.41", - "ultracite": "7.0.8", - "postcss-preset-mantine": "^1.17.0", - "postcss-simple-vars": "^7.0.1", - "typescript": "^5" - }, - "pnpm": { - "overrides": { - "@types/react": "npm:types-react@19.0.0-rc.1", - "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" - } - } -} diff --git a/packages/cli-old/template/nextjs-mantine/postcss.config.cjs b/packages/cli-old/template/nextjs-mantine/postcss.config.cjs deleted file mode 100644 index 085a0ef9..00000000 --- a/packages/cli-old/template/nextjs-mantine/postcss.config.cjs +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - plugins: { - "@tailwindcss/postcss": {}, - "postcss-preset-mantine": {}, - "postcss-simple-vars": { - variables: { - "mantine-breakpoint-xs": "36em", - "mantine-breakpoint-sm": "48em", - "mantine-breakpoint-md": "62em", - "mantine-breakpoint-lg": "75em", - "mantine-breakpoint-xl": "88em", - }, - }, - }, -}; diff --git a/packages/cli-old/template/nextjs-mantine/proofkit.json b/packages/cli-old/template/nextjs-mantine/proofkit.json deleted file mode 100644 index c536f9bf..00000000 --- a/packages/cli-old/template/nextjs-mantine/proofkit.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "auth": { "type": "none" }, - "envFile": ".env", - "appType": "browser", - "ui": "mantine", - "appliedUpgrades": ["cursorRules"] -} diff --git a/packages/cli-old/template/nextjs-mantine/public/favicon.ico b/packages/cli-old/template/nextjs-mantine/public/favicon.ico deleted file mode 100644 index ba9355b8..00000000 Binary files a/packages/cli-old/template/nextjs-mantine/public/favicon.ico and /dev/null differ diff --git a/packages/cli-old/template/nextjs-mantine/public/proofkit.png b/packages/cli-old/template/nextjs-mantine/public/proofkit.png deleted file mode 100644 index 2969f842..00000000 Binary files a/packages/cli-old/template/nextjs-mantine/public/proofkit.png and /dev/null differ diff --git a/packages/cli-old/template/nextjs-mantine/src/app/(main)/layout.tsx b/packages/cli-old/template/nextjs-mantine/src/app/(main)/layout.tsx deleted file mode 100644 index 57a8452b..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/app/(main)/layout.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import AppShell from "@/components/AppShell/internal/AppShell"; -import React from "react"; - -export default function Layout({ children }: { children: React.ReactNode }) { - return {children}; -} diff --git a/packages/cli-old/template/nextjs-mantine/src/app/(main)/page.tsx b/packages/cli-old/template/nextjs-mantine/src/app/(main)/page.tsx deleted file mode 100644 index 3bc6752b..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/app/(main)/page.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { - ActionIcon, - Anchor, - AppShellFooter, - Box, - Code, - Container, - Group, - Image, - px, - Stack, - Text, - Title, -} from "@mantine/core"; -import { IconBrandGithub, IconExternalLink } from "@tabler/icons-react"; - -export default function Home() { - return ( - <> - - - ProofKit - Welcome! - - - This is the base template home page. To add more pages, components, - or other features, run the ProofKit CLI from within your project. - - __PNPM_COMMAND__ proofkit - - - To change this page, open src/app/(main)/page.tsx - - - - ProofKit Docs - - - - - - - - - - Sponsored by{" "} - - Proof - {" "} - and{" "} - - Ottomatic - - - - - - - - - - - - - - - ); -} diff --git a/packages/cli-old/template/nextjs-mantine/src/app/layout.tsx b/packages/cli-old/template/nextjs-mantine/src/app/layout.tsx deleted file mode 100644 index 9512cb63..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/app/layout.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Suspense } from "react"; -import { theme } from "@/config/theme/mantine-theme"; -import { ColorSchemeScript, MantineProvider } from "@mantine/core"; -import { ModalsProvider } from "@mantine/modals"; -import { Notifications } from "@mantine/notifications"; - -import "@mantine/core/styles.css"; -import "@mantine/notifications/styles.css"; -import "@mantine/dates/styles.css"; -import "mantine-react-table/styles.css"; -import "@/config/theme/globals.css"; - -import { type Metadata } from "next"; - -export const metadata: Metadata = { - title: "My ProofKit App", - description: "Generated by proofkit", - icons: [{ rel: "icon", url: "/favicon.ico" }], -}; - -export default function RootLayout({ - children, -}: Readonly<{ children: React.ReactNode }>) { - return ( - - - - - - - - - - {children} - - - - ); -} diff --git a/packages/cli-old/template/nextjs-mantine/src/app/navigation.tsx b/packages/cli-old/template/nextjs-mantine/src/app/navigation.tsx deleted file mode 100644 index 887073db..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/app/navigation.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { type ProofKitRoute } from "@proofkit/cli"; - -export const primaryRoutes: ProofKitRoute[] = [ - { - label: "Dashboard", - type: "link", - href: "/", - exactMatch: true, - }, -]; - -export const secondaryRoutes: ProofKitRoute[] = []; diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppLogo.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppLogo.tsx deleted file mode 100644 index f5ea4966..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppLogo.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { IconInfinity } from "@tabler/icons-react"; -import React from "react"; - -export default function AppLogo() { - return ; -} diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/AppShell.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/AppShell.tsx deleted file mode 100644 index 8c4270df..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/AppShell.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Header } from "@/components/AppShell/internal/Header"; -import { AppShell, AppShellHeader, AppShellMain } from "@mantine/core"; -import React from "react"; - -import { headerHeight } from "./config"; - -export default function MainAppShell({ - children, -}: { - children: React.ReactNode; -}) { - return ( - - -
- - - {children} - - ); -} diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.module.css b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.module.css deleted file mode 100644 index 79d81bad..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.module.css +++ /dev/null @@ -1,40 +0,0 @@ -.header { - /* height: rem(56px); */ - margin-bottom: rem(120px); - background-color: var(--mantine-color-body); - border-bottom: rem(1px) solid - light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); -} - -.inner { - /* height: rem(56px); */ - display: flex; - justify-content: space-between; - align-items: center; -} - -.link { - display: block; - line-height: 1; - padding: rem(8px) rem(12px); - border-radius: var(--mantine-radius-sm); - text-decoration: none; - color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0)); - font-size: var(--mantine-font-size-sm); - font-weight: 500; - cursor: pointer; - background: none; - border: none; - - @mixin hover { - background-color: light-dark( - var(--mantine-color-gray-0), - var(--mantine-color-dark-6) - ); - } - - [data-mantine-color-scheme] &[data-active] { - background-color: var(--mantine-primary-color-filled); - color: var(--mantine-color-white); - } -} diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.tsx deleted file mode 100644 index 4409b1d6..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/Header.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Box, Container, Group } from "@mantine/core"; - -import SlotHeaderCenter from "../slot-header-center"; -import SlotHeaderLeft from "../slot-header-left"; -import SlotHeaderRight from "../slot-header-right"; -import { headerHeight } from "./config"; -import classes from "./Header.module.css"; -import HeaderMobileMenu from "./HeaderMobileMenu"; - -export function Header() { - return ( -
- - - - - - - - - - - - - - -
- ); -} diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderMobileMenu.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderMobileMenu.tsx deleted file mode 100644 index 910104fb..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderMobileMenu.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { Burger, Menu } from "@mantine/core"; -import { useDisclosure } from "@mantine/hooks"; - -import SlotHeaderMobileMenuContent from "../slot-header-mobile-content"; - -export default function HeaderMobileMenu() { - const [opened, { toggle }] = useDisclosure(false); - - return ( - - - - - - - - - ); -} diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderNavLink.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderNavLink.tsx deleted file mode 100644 index 06ce2676..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/HeaderNavLink.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -import { type ProofKitRoute } from "@proofkit/cli"; -import { usePathname } from "next/navigation"; -import React from "react"; - -import classes from "./Header.module.css"; - -export default function HeaderNavLink(route: ProofKitRoute) { - const pathname = usePathname(); - - if (route.type === "function") { - return ( - - ); - } - - const isActive = route.exactMatch - ? pathname === route.href - : pathname.startsWith(route.href); - - if (route.type === "link") { - return ( - - {route.label} - - ); - } -} diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/config.ts b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/config.ts deleted file mode 100644 index ded639d0..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/internal/config.ts +++ /dev/null @@ -1 +0,0 @@ -export const headerHeight = 56; diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-center.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-center.tsx deleted file mode 100644 index 2de3b630..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-center.tsx +++ /dev/null @@ -1,13 +0,0 @@ -/** - * DO NOT REMOVE / RENAME THIS FILE - * - * You may CUSTOMIZE the content of this file, but the ProofKit CLI expects - * this file to exist and may use it to inject content for other components. - * - * If you don't want it to be used, you may return null or an empty fragment - */ -export function SlotHeaderCenter() { - return null; -} - -export default SlotHeaderCenter; diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-left.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-left.tsx deleted file mode 100644 index 781fcbce..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-left.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import Link from "next/link"; - -import AppLogo from "../AppLogo"; - -/** - * DO NOT REMOVE / RENAME THIS FILE - * - * You may CUSTOMIZE the content of this file, but the ProofKit CLI expects this file to exist and - * may use it to inject content for other components. - * - * If you don't want it to be used, you may return null or an empty fragment - */ -export function SlotHeaderLeft() { - return ( - <> - - - - - ); -} - -export default SlotHeaderLeft; diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-mobile-content.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-mobile-content.tsx deleted file mode 100644 index 9943f8a0..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-mobile-content.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; - -import { primaryRoutes } from "@/app/navigation"; -import { Menu } from "@mantine/core"; -import { useRouter } from "next/navigation"; - -/** - * DO NOT REMOVE / RENAME THIS FILE - * - * You may CUSTOMIZE the content of this file, but the ProofKit CLI expects - * this file to exist and may use it to inject content for other components. - * - * If you don't want it to be used, you may return null or an empty fragment - */ -export function SlotHeaderMobileMenuContent({ - closeMenu, -}: { - closeMenu: () => void; -}) { - const router = useRouter(); - return ( - <> - {primaryRoutes.map((route) => ( - { - closeMenu(); - if (route.type === "function") { - route.onClick(); - } else if (route.type === "link") { - router.push(route.href); - } - }} - > - {route.label} - - ))} - - ); -} - -export default SlotHeaderMobileMenuContent; diff --git a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-right.tsx b/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-right.tsx deleted file mode 100644 index 6c392c95..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/components/AppShell/slot-header-right.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { primaryRoutes } from "@/app/navigation"; -import { Group } from "@mantine/core"; - -import HeaderNavLink from "./internal/HeaderNavLink"; - -/** - * DO NOT REMOVE / RENAME THIS FILE - * - * You may CUSTOMIZE the content of this file, but the ProofKit CLI expects - * this file to exist and may use it to inject content for other components. - * - * If you don't want it to be used, you may return null or an empty fragment - */ -export function SlotHeaderRight() { - return ( - <> - - {primaryRoutes.map((route) => ( - - ))} - - - ); -} - -export default SlotHeaderRight; diff --git a/packages/cli-old/template/nextjs-mantine/src/config/env.ts b/packages/cli-old/template/nextjs-mantine/src/config/env.ts deleted file mode 100644 index 3c50ef8d..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/config/env.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createEnv } from "@t3-oss/env-nextjs"; -import { z } from "zod/v4"; - -export const env = createEnv({ - server: { - NODE_ENV: z - .enum(["development", "test", "production"]) - .default("development"), - }, - client: {}, - // For Next.js >= 13.4.4, you only need to destructure client variables: - experimental__runtimeEnv: {}, -}); diff --git a/packages/cli-old/template/nextjs-mantine/src/config/theme/globals.css b/packages/cli-old/template/nextjs-mantine/src/config/theme/globals.css deleted file mode 100644 index 0e2f76bb..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/config/theme/globals.css +++ /dev/null @@ -1,125 +0,0 @@ -/* Add global styles here */ - -@import "tailwindcss" prefix(tw); -@import "tw-animate-css"; - -@custom-variant dark (&:is(.dark *)); - -:root { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.145 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.145 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.985 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.396 0.141 25.723); - --destructive-foreground: oklch(0.637 0.237 25.331); - --border: oklch(0.269 0 0); - --input: oklch(0.269 0 0); - --ring: oklch(0.439 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(0.269 0 0); - --sidebar-ring: oklch(0.439 0 0); -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); -} - -@layer base { - * { - @apply tw:border-border tw:outline-ring/50; - } - body { - @apply tw:bg-background tw:text-foreground; - } -} diff --git a/packages/cli-old/template/nextjs-mantine/src/config/theme/mantine-theme.ts b/packages/cli-old/template/nextjs-mantine/src/config/theme/mantine-theme.ts deleted file mode 100644 index 890db89c..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/config/theme/mantine-theme.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createTheme, type MantineColorsTuple } from "@mantine/core"; - -// generate your own set of colors here: https://mantine.dev/colors-generator -const brandColor: MantineColorsTuple = [ - "#ffebff", - "#f5d5fb", - "#e6a8f3", - "#d779eb", - "#cb51e4", - "#c337e0", - "#c029df", - "#a91cc6", - "#9715b1", - "#84099c", -]; - -export const theme = createTheme({ - primaryColor: "brand", - colors: { - brand: brandColor, - }, -}); diff --git a/packages/cli-old/template/nextjs-mantine/src/server/safe-action.ts b/packages/cli-old/template/nextjs-mantine/src/server/safe-action.ts deleted file mode 100644 index 7f62198a..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/server/safe-action.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createSafeActionClient } from "next-safe-action"; - -export const actionClient = createSafeActionClient(); diff --git a/packages/cli-old/template/nextjs-mantine/src/utils/notification-helpers.ts b/packages/cli-old/template/nextjs-mantine/src/utils/notification-helpers.ts deleted file mode 100644 index b5aa63e3..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/utils/notification-helpers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - showNotification, - type NotificationData, -} from "@mantine/notifications"; - -export function showErrorNotification(): void; -export function showErrorNotification(props: NotificationData): void; -export function showErrorNotification(message: string): void; -export function showErrorNotification(args?: string | NotificationData): void { - const message = - typeof args === "string" ? args : "An unexpected error occurred."; - const defaultProps = typeof args === "string" ? {} : (args ?? {}); - - showNotification({ color: "red", title: "Error", message, ...defaultProps }); -} - -export function showSuccessNotification(): void; -export function showSuccessNotification(props: NotificationData): void; -export function showSuccessNotification(message: string): void; -export function showSuccessNotification( - args?: string | NotificationData, -): void { - const message = typeof args === "string" ? args : "Success!"; - const defaultProps = typeof args === "string" ? {} : (args ?? {}); - - showNotification({ - color: "green", - title: "Success", - message, - ...defaultProps, - }); -} diff --git a/packages/cli-old/template/nextjs-mantine/src/utils/styles.ts b/packages/cli-old/template/nextjs-mantine/src/utils/styles.ts deleted file mode 100644 index 1f4cd38e..00000000 --- a/packages/cli-old/template/nextjs-mantine/src/utils/styles.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: any[]) { - return twMerge(clsx(inputs)); -} diff --git a/packages/cli-old/template/nextjs-mantine/tsconfig.json b/packages/cli-old/template/nextjs-mantine/tsconfig.json deleted file mode 100644 index 51d0dbce..00000000 --- a/packages/cli-old/template/nextjs-mantine/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - }, - "target": "ES2017" - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/packages/cli-old/template/nextjs-shadcn/.claude/CLAUDE.md b/packages/cli-old/template/nextjs-shadcn/.claude/CLAUDE.md deleted file mode 100644 index 869eaac0..00000000 --- a/packages/cli-old/template/nextjs-shadcn/.claude/CLAUDE.md +++ /dev/null @@ -1,327 +0,0 @@ -# Project Context -Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter. - -## Key Principles -- Zero configuration required -- Subsecond performance -- Maximum type safety -- AI-friendly code generation - -## Before Writing Code -1. Analyze existing patterns in the codebase -2. Consider edge cases and error scenarios -3. Follow the rules below strictly -4. Validate accessibility requirements - -## Rules - -### Accessibility (a11y) -- Don't use `accessKey` attribute on any HTML element. -- Don't set `aria-hidden="true"` on focusable elements. -- Don't add ARIA roles, states, and properties to elements that don't support them. -- Don't use distracting elements like `` or ``. -- Only use the `scope` prop on `` elements. -- Don't assign non-interactive ARIA roles to interactive HTML elements. -- Make sure label elements have text content and are associated with an input. -- Don't assign interactive ARIA roles to non-interactive HTML elements. -- Don't assign `tabIndex` to non-interactive HTML elements. -- Don't use positive integers for `tabIndex` property. -- Don't include "image", "picture", or "photo" in img alt prop. -- Don't use explicit role property that's the same as the implicit/default role. -- Make static elements with click handlers use a valid role attribute. -- Always include a `title` element for SVG elements. -- Give all elements requiring alt text meaningful information for screen readers. -- Make sure anchors have content that's accessible to screen readers. -- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`. -- Include all required ARIA attributes for elements with ARIA roles. -- Make sure ARIA properties are valid for the element's supported roles. -- Always include a `type` attribute for button elements. -- Make elements with interactive roles and handlers focusable. -- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`). -- Always include a `lang` attribute on the html element. -- Always include a `title` attribute for iframe elements. -- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`. -- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`. -- Include caption tracks for audio and video elements. -- Use semantic elements instead of role attributes in JSX. -- Make sure all anchors are valid and navigable. -- Ensure all ARIA properties (`aria-*`) are valid. -- Use valid, non-abstract ARIA roles for elements with ARIA roles. -- Use valid ARIA state and property values. -- Use valid values for the `autocomplete` attribute on input elements. -- Use correct ISO language/country codes for the `lang` attribute. - -### Code Complexity and Quality -- Don't use consecutive spaces in regular expression literals. -- Don't use the `arguments` object. -- Don't use primitive type aliases or misleading types. -- Don't use the comma operator. -- Don't use empty type parameters in type aliases and interfaces. -- Don't write functions that exceed a given Cognitive Complexity score. -- Don't nest describe() blocks too deeply in test files. -- Don't use unnecessary boolean casts. -- Don't use unnecessary callbacks with flatMap. -- Use for...of statements instead of Array.forEach. -- Don't create classes that only have static members (like a static namespace). -- Don't use this and super in static contexts. -- Don't use unnecessary catch clauses. -- Don't use unnecessary constructors. -- Don't use unnecessary continue statements. -- Don't export empty modules that don't change anything. -- Don't use unnecessary escape sequences in regular expression literals. -- Don't use unnecessary fragments. -- Don't use unnecessary labels. -- Don't use unnecessary nested block statements. -- Don't rename imports, exports, and destructured assignments to the same name. -- Don't use unnecessary string or template literal concatenation. -- Don't use String.raw in template literals when there are no escape sequences. -- Don't use useless case statements in switch statements. -- Don't use ternary operators when simpler alternatives exist. -- Don't use useless `this` aliasing. -- Don't use any or unknown as type constraints. -- Don't initialize variables to undefined. -- Don't use the void operators (they're not familiar). -- Use arrow functions instead of function expressions. -- Use Date.now() to get milliseconds since the Unix Epoch. -- Use .flatMap() instead of map().flat() when possible. -- Use literal property access instead of computed property access. -- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work. -- Use concise optional chaining instead of chained logical expressions. -- Use regular expression literals instead of the RegExp constructor when possible. -- Don't use number literal object member names that aren't base 10 or use underscore separators. -- Remove redundant terms from logical expressions. -- Use while loops instead of for loops when you don't need initializer and update expressions. -- Don't pass children as props. -- Don't reassign const variables. -- Don't use constant expressions in conditions. -- Don't use `Math.min` and `Math.max` to clamp values when the result is constant. -- Don't return a value from a constructor. -- Don't use empty character classes in regular expression literals. -- Don't use empty destructuring patterns. -- Don't call global object properties as functions. -- Don't declare functions and vars that are accessible outside their block. -- Make sure builtins are correctly instantiated. -- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors. -- Don't use variables and function parameters before they're declared. -- Don't use 8 and 9 escape sequences in string literals. -- Don't use literal numbers that lose precision. - -### React and JSX Best Practices -- Don't use the return value of React.render. -- Make sure all dependencies are correctly specified in React hooks. -- Make sure all React hooks are called from the top level of component functions. -- Don't forget key props in iterators and collection literals. -- Don't destructure props inside JSX components in Solid projects. -- Don't define React components inside other components. -- Don't use event handlers on non-interactive elements. -- Don't assign to React component props. -- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element. -- Don't use dangerous JSX props. -- Don't use Array index in keys. -- Don't insert comments as text nodes. -- Don't assign JSX properties multiple times. -- Don't add extra closing tags for components without children. -- Use `<>...` instead of `...`. -- Watch out for possible "wrong" semicolons inside JSX elements. - -### Correctness and Safety -- Don't assign a value to itself. -- Don't return a value from a setter. -- Don't compare expressions that modify string case with non-compliant values. -- Don't use lexical declarations in switch clauses. -- Don't use variables that haven't been declared in the document. -- Don't write unreachable code. -- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass. -- Don't use control flow statements in finally blocks. -- Don't use optional chaining where undefined values aren't allowed. -- Don't have unused function parameters. -- Don't have unused imports. -- Don't have unused labels. -- Don't have unused private class members. -- Don't have unused variables. -- Make sure void (self-closing) elements don't have children. -- Don't return a value from a function with the return type 'void' -- Use isNaN() when checking for NaN. -- Make sure "for" loop update clauses move the counter in the right direction. -- Make sure typeof expressions are compared to valid values. -- Make sure generator functions contain yield. -- Don't use await inside loops. -- Don't use bitwise operators. -- Don't use expressions where the operation doesn't change the value. -- Make sure Promise-like statements are handled appropriately. -- Don't use __dirname and __filename in the global scope. -- Prevent import cycles. -- Don't use configured elements. -- Don't hardcode sensitive data like API keys and tokens. -- Don't let variable declarations shadow variables from outer scopes. -- Don't use the TypeScript directive @ts-ignore. -- Prevent duplicate polyfills from Polyfill.io. -- Don't use useless backreferences in regular expressions that always match empty strings. -- Don't use unnecessary escapes in string literals. -- Don't use useless undefined. -- Make sure getters and setters for the same property are next to each other in class and object definitions. -- Make sure object literals are declared consistently (defaults to explicit definitions). -- Use static Response methods instead of new Response() constructor when possible. -- Make sure switch-case statements are exhaustive. -- Make sure the `preconnect` attribute is used when using Google Fonts. -- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item. -- Make sure iterable callbacks return consistent values. -- Use `with { type: "json" }` for JSON module imports. -- Use numeric separators in numeric literals. -- Use object spread instead of `Object.assign()` when constructing new objects. -- Always use the radix argument when using `parseInt()`. -- Make sure JSDoc comment lines start with a single asterisk, except for the first one. -- Include a description parameter for `Symbol()`. -- Don't use spread (`...`) syntax on accumulators. -- Don't use the `delete` operator. -- Don't access namespace imports dynamically. -- Don't use namespace imports. -- Declare regex literals at the top level. -- Don't use `target="_blank"` without `rel="noopener"`. - -### TypeScript Best Practices -- Don't use TypeScript enums. -- Don't export imported variables. -- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions. -- Don't use TypeScript namespaces. -- Don't use non-null assertions with the `!` postfix operator. -- Don't use parameter properties in class constructors. -- Don't use user-defined types. -- Use `as const` instead of literal types and type annotations. -- Use either `T[]` or `Array` consistently. -- Initialize each enum member value explicitly. -- Use `export type` for types. -- Use `import type` for types. -- Make sure all enum members are literal values. -- Don't use TypeScript const enum. -- Don't declare empty interfaces. -- Don't let variables evolve into any type through reassignments. -- Don't use the any type. -- Don't misuse the non-null assertion operator (!) in TypeScript files. -- Don't use implicit any type on variable declarations. -- Don't merge interfaces and classes unsafely. -- Don't use overload signatures that aren't next to each other. -- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces. - -### Style and Consistency -- Don't use global `eval()`. -- Don't use callbacks in asynchronous tests and hooks. -- Don't use negation in `if` statements that have `else` clauses. -- Don't use nested ternary expressions. -- Don't reassign function parameters. -- This rule lets you specify global variable names you don't want to use in your application. -- Don't use specified modules when loaded by import or require. -- Don't use constants whose value is the upper-case version of their name. -- Use `String.slice()` instead of `String.substr()` and `String.substring()`. -- Don't use template literals if you don't need interpolation or special-character handling. -- Don't use `else` blocks when the `if` block breaks early. -- Don't use yoda expressions. -- Don't use Array constructors. -- Use `at()` instead of integer index access. -- Follow curly brace conventions. -- Use `else if` instead of nested `if` statements in `else` clauses. -- Use single `if` statements instead of nested `if` clauses. -- Use `new` for all builtins except `String`, `Number`, and `Boolean`. -- Use consistent accessibility modifiers on class properties and methods. -- Use `const` declarations for variables that are only assigned once. -- Put default function parameters and optional function parameters last. -- Include a `default` clause in switch statements. -- Use the `**` operator instead of `Math.pow`. -- Use `for-of` loops when you need the index to extract an item from the iterated array. -- Use `node:assert/strict` over `node:assert`. -- Use the `node:` protocol for Node.js builtin modules. -- Use Number properties instead of global ones. -- Use assignment operator shorthand where possible. -- Use function types instead of object types with call signatures. -- Use template literals over string concatenation. -- Use `new` when throwing an error. -- Don't throw non-Error values. -- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`. -- Use standard constants instead of approximated literals. -- Don't assign values in expressions. -- Don't use async functions as Promise executors. -- Don't reassign exceptions in catch clauses. -- Don't reassign class members. -- Don't compare against -0. -- Don't use labeled statements that aren't loops. -- Don't use void type outside of generic or return types. -- Don't use console. -- Don't use control characters and escape sequences that match control characters in regular expression literals. -- Don't use debugger. -- Don't assign directly to document.cookie. -- Use `===` and `!==`. -- Don't use duplicate case labels. -- Don't use duplicate class members. -- Don't use duplicate conditions in if-else-if chains. -- Don't use two keys with the same name inside objects. -- Don't use duplicate function parameter names. -- Don't have duplicate hooks in describe blocks. -- Don't use empty block statements and static blocks. -- Don't let switch clauses fall through. -- Don't reassign function declarations. -- Don't allow assignments to native objects and read-only global variables. -- Use Number.isFinite instead of global isFinite. -- Use Number.isNaN instead of global isNaN. -- Don't assign to imported bindings. -- Don't use irregular whitespace characters. -- Don't use labels that share a name with a variable. -- Don't use characters made with multiple code points in character class syntax. -- Make sure to use new and constructor properly. -- Don't use shorthand assign when the variable appears on both sides. -- Don't use octal escape sequences in string literals. -- Don't use Object.prototype builtins directly. -- Don't redeclare variables, functions, classes, and types in the same scope. -- Don't have redundant "use strict". -- Don't compare things where both sides are exactly the same. -- Don't let identifiers shadow restricted names. -- Don't use sparse arrays (arrays with holes). -- Don't use template literal placeholder syntax in regular strings. -- Don't use the then property. -- Don't use unsafe negation. -- Don't use var. -- Don't use with statements in non-strict contexts. -- Make sure async functions actually use await. -- Make sure default clauses in switch statements come last. -- Make sure to pass a message value when creating a built-in error. -- Make sure get methods always return a value. -- Use a recommended display strategy with Google Fonts. -- Make sure for-in loops include an if statement. -- Use Array.isArray() instead of instanceof Array. -- Make sure to use the digits argument with Number#toFixed(). -- Make sure to use the "use strict" directive in script files. - -### Next.js Specific Rules -- Don't use `` elements in Next.js projects. -- Don't use `` elements in Next.js projects. -- Don't import next/document outside of pages/_document.jsx in Next.js projects. -- Don't use the next/head module in pages/_document.js on Next.js projects. - -### Testing Best Practices -- Don't use export or module.exports in test files. -- Don't use focused tests. -- Make sure the assertion function, like expect, is placed inside an it() function call. -- Don't use disabled tests. - -## Common Tasks -- `npx ultracite init` - Initialize Ultracite in your project -- `npx ultracite format` - Format and fix code automatically -- `npx ultracite lint` - Check for issues without fixing - -## Example: Error Handling -```typescript -// โœ… Good: Comprehensive error handling -try { - const result = await fetchData(); - return { success: true, data: result }; -} catch (error) { - console.error('API call failed:', error); - return { success: false, error: error.message }; -} - -// โŒ Bad: Swallowing errors -try { - return await fetchData(); -} catch (e) { - console.log(e); -} -``` \ No newline at end of file diff --git a/packages/cli-old/template/nextjs-shadcn/.cursor/rules/ultracite.mdc b/packages/cli-old/template/nextjs-shadcn/.cursor/rules/ultracite.mdc deleted file mode 100644 index 98495535..00000000 --- a/packages/cli-old/template/nextjs-shadcn/.cursor/rules/ultracite.mdc +++ /dev/null @@ -1,333 +0,0 @@ ---- -description: Ultracite Rules - AI-Ready Formatter and Linter -globs: "**/*.{ts,tsx,js,jsx}" -alwaysApply: true ---- - -# Project Context -Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter. - -## Key Principles -- Zero configuration required -- Subsecond performance -- Maximum type safety -- AI-friendly code generation - -## Before Writing Code -1. Analyze existing patterns in the codebase -2. Consider edge cases and error scenarios -3. Follow the rules below strictly -4. Validate accessibility requirements - -## Rules - -### Accessibility (a11y) -- Don't use `accessKey` attribute on any HTML element. -- Don't set `aria-hidden="true"` on focusable elements. -- Don't add ARIA roles, states, and properties to elements that don't support them. -- Don't use distracting elements like `` or ``. -- Only use the `scope` prop on `` elements. -- Don't assign non-interactive ARIA roles to interactive HTML elements. -- Make sure label elements have text content and are associated with an input. -- Don't assign interactive ARIA roles to non-interactive HTML elements. -- Don't assign `tabIndex` to non-interactive HTML elements. -- Don't use positive integers for `tabIndex` property. -- Don't include "image", "picture", or "photo" in img alt prop. -- Don't use explicit role property that's the same as the implicit/default role. -- Make static elements with click handlers use a valid role attribute. -- Always include a `title` element for SVG elements. -- Give all elements requiring alt text meaningful information for screen readers. -- Make sure anchors have content that's accessible to screen readers. -- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`. -- Include all required ARIA attributes for elements with ARIA roles. -- Make sure ARIA properties are valid for the element's supported roles. -- Always include a `type` attribute for button elements. -- Make elements with interactive roles and handlers focusable. -- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`). -- Always include a `lang` attribute on the html element. -- Always include a `title` attribute for iframe elements. -- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`. -- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`. -- Include caption tracks for audio and video elements. -- Use semantic elements instead of role attributes in JSX. -- Make sure all anchors are valid and navigable. -- Ensure all ARIA properties (`aria-*`) are valid. -- Use valid, non-abstract ARIA roles for elements with ARIA roles. -- Use valid ARIA state and property values. -- Use valid values for the `autocomplete` attribute on input elements. -- Use correct ISO language/country codes for the `lang` attribute. - -### Code Complexity and Quality -- Don't use consecutive spaces in regular expression literals. -- Don't use the `arguments` object. -- Don't use primitive type aliases or misleading types. -- Don't use the comma operator. -- Don't use empty type parameters in type aliases and interfaces. -- Don't write functions that exceed a given Cognitive Complexity score. -- Don't nest describe() blocks too deeply in test files. -- Don't use unnecessary boolean casts. -- Don't use unnecessary callbacks with flatMap. -- Use for...of statements instead of Array.forEach. -- Don't create classes that only have static members (like a static namespace). -- Don't use this and super in static contexts. -- Don't use unnecessary catch clauses. -- Don't use unnecessary constructors. -- Don't use unnecessary continue statements. -- Don't export empty modules that don't change anything. -- Don't use unnecessary escape sequences in regular expression literals. -- Don't use unnecessary fragments. -- Don't use unnecessary labels. -- Don't use unnecessary nested block statements. -- Don't rename imports, exports, and destructured assignments to the same name. -- Don't use unnecessary string or template literal concatenation. -- Don't use String.raw in template literals when there are no escape sequences. -- Don't use useless case statements in switch statements. -- Don't use ternary operators when simpler alternatives exist. -- Don't use useless `this` aliasing. -- Don't use any or unknown as type constraints. -- Don't initialize variables to undefined. -- Don't use the void operators (they're not familiar). -- Use arrow functions instead of function expressions. -- Use Date.now() to get milliseconds since the Unix Epoch. -- Use .flatMap() instead of map().flat() when possible. -- Use literal property access instead of computed property access. -- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work. -- Use concise optional chaining instead of chained logical expressions. -- Use regular expression literals instead of the RegExp constructor when possible. -- Don't use number literal object member names that aren't base 10 or use underscore separators. -- Remove redundant terms from logical expressions. -- Use while loops instead of for loops when you don't need initializer and update expressions. -- Don't pass children as props. -- Don't reassign const variables. -- Don't use constant expressions in conditions. -- Don't use `Math.min` and `Math.max` to clamp values when the result is constant. -- Don't return a value from a constructor. -- Don't use empty character classes in regular expression literals. -- Don't use empty destructuring patterns. -- Don't call global object properties as functions. -- Don't declare functions and vars that are accessible outside their block. -- Make sure builtins are correctly instantiated. -- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors. -- Don't use variables and function parameters before they're declared. -- Don't use 8 and 9 escape sequences in string literals. -- Don't use literal numbers that lose precision. - -### React and JSX Best Practices -- Don't use the return value of React.render. -- Make sure all dependencies are correctly specified in React hooks. -- Make sure all React hooks are called from the top level of component functions. -- Don't forget key props in iterators and collection literals. -- Don't destructure props inside JSX components in Solid projects. -- Don't define React components inside other components. -- Don't use event handlers on non-interactive elements. -- Don't assign to React component props. -- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element. -- Don't use dangerous JSX props. -- Don't use Array index in keys. -- Don't insert comments as text nodes. -- Don't assign JSX properties multiple times. -- Don't add extra closing tags for components without children. -- Use `<>...` instead of `...`. -- Watch out for possible "wrong" semicolons inside JSX elements. - -### Correctness and Safety -- Don't assign a value to itself. -- Don't return a value from a setter. -- Don't compare expressions that modify string case with non-compliant values. -- Don't use lexical declarations in switch clauses. -- Don't use variables that haven't been declared in the document. -- Don't write unreachable code. -- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass. -- Don't use control flow statements in finally blocks. -- Don't use optional chaining where undefined values aren't allowed. -- Don't have unused function parameters. -- Don't have unused imports. -- Don't have unused labels. -- Don't have unused private class members. -- Don't have unused variables. -- Make sure void (self-closing) elements don't have children. -- Don't return a value from a function with the return type 'void' -- Use isNaN() when checking for NaN. -- Make sure "for" loop update clauses move the counter in the right direction. -- Make sure typeof expressions are compared to valid values. -- Make sure generator functions contain yield. -- Don't use await inside loops. -- Don't use bitwise operators. -- Don't use expressions where the operation doesn't change the value. -- Make sure Promise-like statements are handled appropriately. -- Don't use __dirname and __filename in the global scope. -- Prevent import cycles. -- Don't use configured elements. -- Don't hardcode sensitive data like API keys and tokens. -- Don't let variable declarations shadow variables from outer scopes. -- Don't use the TypeScript directive @ts-ignore. -- Prevent duplicate polyfills from Polyfill.io. -- Don't use useless backreferences in regular expressions that always match empty strings. -- Don't use unnecessary escapes in string literals. -- Don't use useless undefined. -- Make sure getters and setters for the same property are next to each other in class and object definitions. -- Make sure object literals are declared consistently (defaults to explicit definitions). -- Use static Response methods instead of new Response() constructor when possible. -- Make sure switch-case statements are exhaustive. -- Make sure the `preconnect` attribute is used when using Google Fonts. -- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item. -- Make sure iterable callbacks return consistent values. -- Use `with { type: "json" }` for JSON module imports. -- Use numeric separators in numeric literals. -- Use object spread instead of `Object.assign()` when constructing new objects. -- Always use the radix argument when using `parseInt()`. -- Make sure JSDoc comment lines start with a single asterisk, except for the first one. -- Include a description parameter for `Symbol()`. -- Don't use spread (`...`) syntax on accumulators. -- Don't use the `delete` operator. -- Don't access namespace imports dynamically. -- Don't use namespace imports. -- Declare regex literals at the top level. -- Don't use `target="_blank"` without `rel="noopener"`. - -### TypeScript Best Practices -- Don't use TypeScript enums. -- Don't export imported variables. -- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions. -- Don't use TypeScript namespaces. -- Don't use non-null assertions with the `!` postfix operator. -- Don't use parameter properties in class constructors. -- Don't use user-defined types. -- Use `as const` instead of literal types and type annotations. -- Use either `T[]` or `Array` consistently. -- Initialize each enum member value explicitly. -- Use `export type` for types. -- Use `import type` for types. -- Make sure all enum members are literal values. -- Don't use TypeScript const enum. -- Don't declare empty interfaces. -- Don't let variables evolve into any type through reassignments. -- Don't use the any type. -- Don't misuse the non-null assertion operator (!) in TypeScript files. -- Don't use implicit any type on variable declarations. -- Don't merge interfaces and classes unsafely. -- Don't use overload signatures that aren't next to each other. -- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces. - -### Style and Consistency -- Don't use global `eval()`. -- Don't use callbacks in asynchronous tests and hooks. -- Don't use negation in `if` statements that have `else` clauses. -- Don't use nested ternary expressions. -- Don't reassign function parameters. -- This rule lets you specify global variable names you don't want to use in your application. -- Don't use specified modules when loaded by import or require. -- Don't use constants whose value is the upper-case version of their name. -- Use `String.slice()` instead of `String.substr()` and `String.substring()`. -- Don't use template literals if you don't need interpolation or special-character handling. -- Don't use `else` blocks when the `if` block breaks early. -- Don't use yoda expressions. -- Don't use Array constructors. -- Use `at()` instead of integer index access. -- Follow curly brace conventions. -- Use `else if` instead of nested `if` statements in `else` clauses. -- Use single `if` statements instead of nested `if` clauses. -- Use `new` for all builtins except `String`, `Number`, and `Boolean`. -- Use consistent accessibility modifiers on class properties and methods. -- Use `const` declarations for variables that are only assigned once. -- Put default function parameters and optional function parameters last. -- Include a `default` clause in switch statements. -- Use the `**` operator instead of `Math.pow`. -- Use `for-of` loops when you need the index to extract an item from the iterated array. -- Use `node:assert/strict` over `node:assert`. -- Use the `node:` protocol for Node.js builtin modules. -- Use Number properties instead of global ones. -- Use assignment operator shorthand where possible. -- Use function types instead of object types with call signatures. -- Use template literals over string concatenation. -- Use `new` when throwing an error. -- Don't throw non-Error values. -- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`. -- Use standard constants instead of approximated literals. -- Don't assign values in expressions. -- Don't use async functions as Promise executors. -- Don't reassign exceptions in catch clauses. -- Don't reassign class members. -- Don't compare against -0. -- Don't use labeled statements that aren't loops. -- Don't use void type outside of generic or return types. -- Don't use console. -- Don't use control characters and escape sequences that match control characters in regular expression literals. -- Don't use debugger. -- Don't assign directly to document.cookie. -- Use `===` and `!==`. -- Don't use duplicate case labels. -- Don't use duplicate class members. -- Don't use duplicate conditions in if-else-if chains. -- Don't use two keys with the same name inside objects. -- Don't use duplicate function parameter names. -- Don't have duplicate hooks in describe blocks. -- Don't use empty block statements and static blocks. -- Don't let switch clauses fall through. -- Don't reassign function declarations. -- Don't allow assignments to native objects and read-only global variables. -- Use Number.isFinite instead of global isFinite. -- Use Number.isNaN instead of global isNaN. -- Don't assign to imported bindings. -- Don't use irregular whitespace characters. -- Don't use labels that share a name with a variable. -- Don't use characters made with multiple code points in character class syntax. -- Make sure to use new and constructor properly. -- Don't use shorthand assign when the variable appears on both sides. -- Don't use octal escape sequences in string literals. -- Don't use Object.prototype builtins directly. -- Don't redeclare variables, functions, classes, and types in the same scope. -- Don't have redundant "use strict". -- Don't compare things where both sides are exactly the same. -- Don't let identifiers shadow restricted names. -- Don't use sparse arrays (arrays with holes). -- Don't use template literal placeholder syntax in regular strings. -- Don't use the then property. -- Don't use unsafe negation. -- Don't use var. -- Don't use with statements in non-strict contexts. -- Make sure async functions actually use await. -- Make sure default clauses in switch statements come last. -- Make sure to pass a message value when creating a built-in error. -- Make sure get methods always return a value. -- Use a recommended display strategy with Google Fonts. -- Make sure for-in loops include an if statement. -- Use Array.isArray() instead of instanceof Array. -- Make sure to use the digits argument with Number#toFixed(). -- Make sure to use the "use strict" directive in script files. - -### Next.js Specific Rules -- Don't use `` elements in Next.js projects. -- Don't use `` elements in Next.js projects. -- Don't import next/document outside of pages/_document.jsx in Next.js projects. -- Don't use the next/head module in pages/_document.js on Next.js projects. - -### Testing Best Practices -- Don't use export or module.exports in test files. -- Don't use focused tests. -- Make sure the assertion function, like expect, is placed inside an it() function call. -- Don't use disabled tests. - -## Common Tasks -- `npx ultracite init` - Initialize Ultracite in your project -- `npx ultracite format` - Format and fix code automatically -- `npx ultracite lint` - Check for issues without fixing - -## Example: Error Handling -```typescript -// โœ… Good: Comprehensive error handling -try { - const result = await fetchData(); - return { success: true, data: result }; -} catch (error) { - console.error('API call failed:', error); - return { success: false, error: error.message }; -} - -// โŒ Bad: Swallowing errors -try { - return await fetchData(); -} catch (e) { - console.log(e); -} -``` \ No newline at end of file diff --git a/packages/cli-old/template/nextjs-shadcn/.vscode/settings.json b/packages/cli-old/template/nextjs-shadcn/.vscode/settings.json deleted file mode 100644 index 1043bea0..00000000 --- a/packages/cli-old/template/nextjs-shadcn/.vscode/settings.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "editor.defaultFormatter": "esbenp.prettier-vscode", - "[javascript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[json]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[jsonc]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[css]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[graphql]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "typescript.tsdk": "node_modules/typescript/lib", - "editor.formatOnSave": true, - "editor.formatOnPaste": true, - "emmet.showExpandedAbbreviation": "never", - "editor.codeActionsOnSave": { - "source.fixAll.biome": "explicit", - "source.organizeImports.biome": "explicit" - } -} \ No newline at end of file diff --git a/packages/cli-old/template/nextjs-shadcn/README.md b/packages/cli-old/template/nextjs-shadcn/README.md deleted file mode 100644 index 15794a3d..00000000 --- a/packages/cli-old/template/nextjs-shadcn/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# ProofKit NextJS Template - -This is a [NextJS](https://nextjs.org/) project bootstrapped with `@proofkit/cli`. Learn more at [proofkit.proof.sh](https://proofkit.proof.sh) - -## What's next? How do I make an app with this? - -While this template is designed to be a minimal starting point, the proofkit CLI will guide you through adding additional features and pages. - -To add new things to your project, simply run the `proofkit` script from the project's root directory. - -e.g. `npm run proofkit` or `pnpm proofkit` etc. - -For more information, see the full [ProofKit documentation](https://proofkit.proof.sh). - -## Project Structure - -ProofKit projects have an opinionated structure to help you get started and some conventions must be maintained to ensure that the CLI can properly inject new features and components. - -The `src` directory is the home for your application code. It is used for most things except for configuration and is organized as follows: - -- `app` - NextJS app router, where your pages and routes are defined -- `components` - Shared components used throughout the app -- `server` - Code that connects to backend databases and services that should not be exposed in the browser - -Anytime you see an `internal` folder, you should not modify any files inside. These files are maintained exclusively by the ProofKit CLI and changes to them may be overwritten. - -Anytime you see a componet file that begins with `slot-`, you _may_ modify the content, but do not rename, remove, or move them. These are desigend to be customized, but are still used by the CLI to inject additional content. If a slot is not needed by your app, you can have the compoment return `null` or an empty fragment: `<>` diff --git a/packages/cli-old/template/nextjs-shadcn/_gitignore b/packages/cli-old/template/nextjs-shadcn/_gitignore deleted file mode 100644 index 00bba9bb..00000000 --- a/packages/cli-old/template/nextjs-shadcn/_gitignore +++ /dev/null @@ -1,37 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local -.env - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/packages/cli-old/template/nextjs-shadcn/biome.json b/packages/cli-old/template/nextjs-shadcn/biome.json deleted file mode 100644 index 3ac108f5..00000000 --- a/packages/cli-old/template/nextjs-shadcn/biome.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "root": false, - "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "files": { - "ignoreUnknown": true, - "includes": ["**", "!node_modules", "!.next", "!dist", "!build"] - }, - "formatter": { - "enabled": true, - "indentStyle": "space", - "indentWidth": 2 - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "style": { - "noParameterAssign": "error", - "useAsConstAssertion": "error", - "useDefaultParameterLast": "error", - "useEnumInitializers": "error", - "useSelfClosingElements": "error", - "useSingleVarDeclarator": "error", - "noUnusedTemplateLiteral": "error", - "useNumberNamespace": "error", - "noInferrableTypes": "error", - "noUselessElse": "error" - } - }, - "domains": { - "next": "recommended", - "react": "recommended" - } - }, - "assist": { - "actions": { - "source": { - "organizeImports": "on" - } - } - }, - "extends": ["ultracite"] -} diff --git a/packages/cli-old/template/nextjs-shadcn/components.json b/packages/cli-old/template/nextjs-shadcn/components.json deleted file mode 100644 index ffe928f5..00000000 --- a/packages/cli-old/template/nextjs-shadcn/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/app/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} \ No newline at end of file diff --git a/packages/cli-old/template/nextjs-shadcn/next.config.ts b/packages/cli-old/template/nextjs-shadcn/next.config.ts deleted file mode 100644 index 4e6fb1fe..00000000 --- a/packages/cli-old/template/nextjs-shadcn/next.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { NextConfig } from 'next'; -import '@/lib/env'; - -const nextConfig: NextConfig = { - /* config options here */ -}; - -export default nextConfig; diff --git a/packages/cli-old/template/nextjs-shadcn/package.json b/packages/cli-old/template/nextjs-shadcn/package.json deleted file mode 100644 index a61be86e..00000000 --- a/packages/cli-old/template/nextjs-shadcn/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "raw-next", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev --turbopack", - "build": "next build --turbopack", - "proofkit": "proofkit", - "start": "next start", - "lint": "biome check", - "format": "biome format --write" - }, - "dependencies": { - "@radix-ui/react-slot": "^1.2.3", - "@t3-oss/env-nextjs": "^0.13.8", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "lucide-react": "^0.541.0", - "next": "^15.5.8", - "next-themes": "^0.4.6", - "radix-ui": "^1.4.2", - "react": "19.1.1", - "react-dom": "19.1.1", - "sonner": "^2.0.4", - "tailwind-merge": "^3.3.1" - }, - "devDependencies": { - "@biomejs/biome": "2.3.11", - "@tailwindcss/postcss": "^4", - "@types/node": "^22", - "@types/react": "^19", - "@types/react-dom": "^19", - "tailwindcss": "^4", - "tw-animate-css": "^1.3.7", - "typescript": "^5", - "ultracite": "5.4.5" - } -} diff --git a/packages/cli-old/template/nextjs-shadcn/postcss.config.mjs b/packages/cli-old/template/nextjs-shadcn/postcss.config.mjs deleted file mode 100644 index c7bcb4b1..00000000 --- a/packages/cli-old/template/nextjs-shadcn/postcss.config.mjs +++ /dev/null @@ -1,5 +0,0 @@ -const config = { - plugins: ["@tailwindcss/postcss"], -}; - -export default config; diff --git a/packages/cli-old/template/nextjs-shadcn/proofkit.json b/packages/cli-old/template/nextjs-shadcn/proofkit.json deleted file mode 100644 index 13d3916d..00000000 --- a/packages/cli-old/template/nextjs-shadcn/proofkit.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ui": "shadcn", - "envFile": ".env", - "appType": "browser", - "registryTemplates": ["utils/t3-env"] -} diff --git a/packages/cli-old/template/nextjs-shadcn/public/favicon.ico b/packages/cli-old/template/nextjs-shadcn/public/favicon.ico deleted file mode 100644 index ba9355b8..00000000 Binary files a/packages/cli-old/template/nextjs-shadcn/public/favicon.ico and /dev/null differ diff --git a/packages/cli-old/template/nextjs-shadcn/public/proofkit.png b/packages/cli-old/template/nextjs-shadcn/public/proofkit.png deleted file mode 100644 index 2969f842..00000000 Binary files a/packages/cli-old/template/nextjs-shadcn/public/proofkit.png and /dev/null differ diff --git a/packages/cli-old/template/nextjs-shadcn/src/app/(main)/layout.tsx b/packages/cli-old/template/nextjs-shadcn/src/app/(main)/layout.tsx deleted file mode 100644 index 57a8452b..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/app/(main)/layout.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import AppShell from "@/components/AppShell/internal/AppShell"; -import React from "react"; - -export default function Layout({ children }: { children: React.ReactNode }) { - return {children}; -} diff --git a/packages/cli-old/template/nextjs-shadcn/src/app/(main)/page.tsx b/packages/cli-old/template/nextjs-shadcn/src/app/(main)/page.tsx deleted file mode 100644 index be337605..00000000 --- a/packages/cli-old/template/nextjs-shadcn/src/app/(main)/page.tsx +++ /dev/null @@ -1,137 +0,0 @@ -'use client'; - -import { - CheckIcon, - CopyIcon, - ExternalLinkIcon, - TerminalIcon, -} from 'lucide-react'; -import { useState } from 'react'; -import { Button } from '@/components/ui/button'; - -function GitHubMark({ size = 20 }: { size?: number }) { - return ( - - ); -} - -function InlineSnippet({ command }: { command: string }) { - const [copied, setCopied] = useState(false); - - const onCopy = () => { - if (typeof window === 'undefined' || !navigator.clipboard?.writeText) { - return; - } - navigator.clipboard.writeText(command).then( - () => { - setCopied(true); - const timeoutInMilliseconds = 2000; - setTimeout(() => setCopied(false), timeoutInMilliseconds); - }, - () => { - // do nothing - } - ); - }; - - return ( -
-
- -
- - {command} - -
- -
-
- ); -} - -export default function Home() { - return ( -
-
-
- {/** biome-ignore lint/performance/noImgElement: just a template image */} - ProofKit -

Welcome!

- -

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

- - - -

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

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

- React + TypeScript + Vite -

-

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

-

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

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

{step.title}

-

{step.body}

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

React Query ready

-

TanStack Query is preconfigured

-

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

- -
- {hintQuery.isLoading ? "Loading starter data..." : hintQuery.data} -
- -
- - Back to starter - -
-
-
- ); -} diff --git a/packages/cli-old/template/vite-wv/tsconfig.json b/packages/cli-old/template/vite-wv/tsconfig.json deleted file mode 100644 index 1565cf30..00000000 --- a/packages/cli-old/template/vite-wv/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "esModuleInterop": true, - "jsx": "react-jsx", - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "baseUrl": ".", - "noEmit": true, - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src"] -} diff --git a/packages/cli-old/template/vite-wv/vite.config.ts b/packages/cli-old/template/vite-wv/vite.config.ts deleted file mode 100644 index 8e23f082..00000000 --- a/packages/cli-old/template/vite-wv/vite.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import path from "node:path"; -import react from "@vitejs/plugin-react"; -import { fmBridge } from "@proofkit/webviewer/vite-plugins"; -import tailwindcss from "@tailwindcss/vite"; -import { defineConfig } from "vite"; -import { viteSingleFile } from "vite-plugin-singlefile"; - -export default defineConfig({ - server: { - port: 5175, - }, - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, - plugins: [fmBridge(), react(), tailwindcss(), viteSingleFile()], -}); diff --git a/packages/cli-old/tests/browser-apps.smoke.test.ts b/packages/cli-old/tests/browser-apps.smoke.test.ts deleted file mode 100644 index 6ebf69e9..00000000 --- a/packages/cli-old/tests/browser-apps.smoke.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { execSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; -import { join } from "node:path"; -import { beforeEach, describe, expect, it } from "vitest"; -import { z } from "zod/v4"; - -import { verifySmokeProjectBuilds } from "./test-utils"; - -const smokeEnvSchema = z.object({ - OTTO_SERVER_URL: z.url(), - OTTO_ADMIN_API_KEY: z.string().min(1), - FM_DATA_API_KEY: z.string().min(1), - FM_FILE_NAME: z.string().min(1), - FM_LAYOUT_NAME: z.string().min(1), -}); - -const parsedSmokeEnv = smokeEnvSchema.safeParse(process.env); -const describeWhenSmokeEnvPresent = parsedSmokeEnv.success ? describe : describe.skip; - -if (!parsedSmokeEnv.success) { - const missingKeys = [...new Set(parsedSmokeEnv.error.issues.map((issue) => issue.path.join(".")))]; - console.warn(`Skipping external integration smoke tests; missing required env vars: ${missingKeys.join(", ")}`); -} - -describeWhenSmokeEnvPresent("External integration smoke tests (non-interactive CLI)", () => { - if (!parsedSmokeEnv.success) { - return; - } - - // Use root-level tmp directory for test outputs - const testDir = join(__dirname, "..", "..", "tmp", "cli-tests"); - const cliPath = join(__dirname, "..", "dist", "index.js"); - const projectName = "test-fm-project"; - const projectDir = join(testDir, projectName); - - // Required for live Otto/FileMaker integration smoke coverage. - const testEnv = parsedSmokeEnv.data; - - beforeEach( - () => { - // Clean up any stale test project from previous runs - if (existsSync(projectDir)) { - rmSync(projectDir, { recursive: true, force: true }); - } - // Ensure the test directory exists - mkdirSync(testDir, { recursive: true }); - }, - 30_000, // 30s timeout for cleanup of large node_modules - ); - - it("should create a browser project with FileMaker integration in non-interactive mode", () => { - // Build the command with all necessary flags for non-interactive mode - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--appType browser", - "--dataSource filemaker", - `--server "${testEnv.OTTO_SERVER_URL}"`, - `--adminApiKey "${testEnv.OTTO_ADMIN_API_KEY}"`, - `--dataApiKey "${testEnv.FM_DATA_API_KEY}"`, - `--fileName "${testEnv.FM_FILE_NAME}"`, - `--layoutName "${testEnv.FM_LAYOUT_NAME}"`, - "--noGit", // Skip git initialization for testing - ].join(" "); - - // Execute the command - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - }); - }).not.toThrow(); - - // Verify project structure - expect(existsSync(projectDir)).toBe(true); - expect(existsSync(join(projectDir, "package.json"))).toBe(true); - expect(existsSync(join(projectDir, "proofkit.json"))).toBe(true); - expect(existsSync(join(projectDir, ".env"))).toBe(true); - - // Verify package.json content - const pkgJson = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf-8")); - expect(pkgJson.name).toBe(projectName); - - // Verify proofkit.json content - const proofkitConfig = JSON.parse(readFileSync(join(projectDir, "proofkit.json"), "utf-8")); - expect(proofkitConfig.appType).toBe("browser"); - expect(proofkitConfig.dataSources).toContainEqual( - expect.objectContaining({ - type: "fm", - name: "filemaker", - }), - ); - - // Verify the project can be built successfully - verifySmokeProjectBuilds(projectDir); - }); -}); diff --git a/packages/cli-old/tests/cli.test.ts b/packages/cli-old/tests/cli.test.ts deleted file mode 100644 index f86d3fb4..00000000 --- a/packages/cli-old/tests/cli.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { execSync } from "node:child_process"; -import { describe, expect, it } from "vitest"; - -describe("CLI Basic Tests", () => { - it("should show help without throwing", () => { - expect(() => { - execSync("node ../dist/index.js --help", { - cwd: import.meta.dirname, - encoding: "utf-8", - }); - }).not.toThrow(); - }); - - it("should be executable", () => { - expect(() => { - execSync("node ../dist/index.js --version", { - cwd: import.meta.dirname, - encoding: "utf-8", - }); - }).not.toThrow(); - }); -}); diff --git a/packages/cli-old/tests/init-non-interactive-failures.test.ts b/packages/cli-old/tests/init-non-interactive-failures.test.ts deleted file mode 100644 index af855a83..00000000 --- a/packages/cli-old/tests/init-non-interactive-failures.test.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import { beforeEach, describe, expect, it } from "vitest"; - -type ExecFailure = Error & { - status?: number | null; - stdout?: string | Buffer; - stderr?: string | Buffer; -}; -const typegenCommandPattern = /\b(?:npm run|pnpm|yarn|bun)\s+typegen\b/; - -function toText(value: string | Buffer | undefined) { - if (typeof value === "string") { - return value; - } - if (!value) { - return ""; - } - return value.toString("utf-8"); -} - -describe("Init Non-Interactive Failure Paths", () => { - const cliRoot = join(__dirname, ".."); - const testDir = join(__dirname, "..", "..", "tmp", "init-failure-tests"); - const cliPath = join(__dirname, "..", "dist", "index.js"); - - beforeEach(() => { - rmSync(testDir, { recursive: true, force: true }); - mkdirSync(testDir, { recursive: true }); - }); - - const rebuildCli = () => { - execFileSync("pnpm", ["build"], { - cwd: cliRoot, - env: process.env, - stdio: "pipe", - }); - }; - - const runInitCommand = (args: string[], cwd = testDir) => { - const execute = () => - execFileSync("node", [cliPath, "init", ...args], { - cwd, - env: process.env, - stdio: "pipe", - encoding: "utf-8", - }); - - try { - return execute(); - } catch (error) { - const failure = error as ExecFailure; - const output = `${toText(failure.stdout)}\n${toText(failure.stderr)}`; - if (output.includes("Cannot find module") && output.includes("dist/index.js")) { - rebuildCli(); - return execute(); - } - throw error; - } - }; - - const runInitExpectFailure = (args: string[], cwd = testDir) => { - try { - runInitCommand(args, cwd); - throw new Error(`Expected init to fail, but it succeeded: ${args.join(" ")}`); - } catch (error) { - const failure = error as ExecFailure; - if (typeof failure.status === "number" || failure.status === null) { - return { - status: failure.status, - stdout: toText(failure.stdout), - stderr: toText(failure.stderr), - }; - } - throw error; - } - }; - - const runInitExpectSuccess = (args: string[], cwd = testDir) => runInitCommand(args, cwd); - - it("fails in non-interactive mode without a project name and does not scaffold", () => { - writeFileSync(join(testDir, "sentinel.txt"), "keep"); - - const result = runInitExpectFailure(["--non-interactive", "--appType", "webviewer", "--noInstall", "--noGit"]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("Project name is required in non-interactive mode."); - expect(readdirSync(testDir).sort()).toEqual(["sentinel.txt"]); - }); - - it("fails fast for invalid non-interactive app names and does not create a project directory", () => { - const projectName = "Bad Name"; - - const result = runInitExpectFailure([ - projectName, - "--non-interactive", - "--appType", - "webviewer", - "--noInstall", - "--noGit", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("Name must consist of only lowercase alphanumeric characters, '-', and '_'"); - expect(existsSync(join(testDir, projectName))).toBe(false); - }); - - it("fails for invalid scoped-path edge cases before mutating the target directory", () => { - writeFileSync(join(testDir, "README.md"), "existing content"); - - const result = runInitExpectFailure([ - "@scope", - "--non-interactive", - "--appType", - "webviewer", - "--noInstall", - "--noGit", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("Name must consist of only lowercase alphanumeric characters, '-', and '_'"); - expect(readFileSync(join(testDir, "README.md"), "utf-8")).toBe("existing content"); - expect(existsSync(join(testDir, "package.json"))).toBe(false); - expect(existsSync(join(testDir, "proofkit.json"))).toBe(false); - }); - - it("fails for partial FileMaker schema flags without creating a scaffold", () => { - const projectName = "partial-filemaker-flags"; - - const result = runInitExpectFailure([ - projectName, - "--non-interactive", - "--appType", - "webviewer", - "--dataSource", - "filemaker", - "--layoutName", - "Contacts", - "--noInstall", - "--noGit", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("Both --layoutName and --schemaName must be provided together."); - expect(existsSync(join(testDir, projectName))).toBe(false); - }); - - it("fails when FileMaker flags are passed without selecting the filemaker data source", () => { - const projectName = "unsupported-filemaker-flags"; - - const result = runInitExpectFailure([ - projectName, - "--non-interactive", - "--appType", - "webviewer", - "--layoutName", - "Contacts", - "--schemaName", - "Contacts", - "--noInstall", - "--noGit", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("FileMaker flags require --dataSource filemaker in non-interactive mode."); - expect(existsSync(join(testDir, projectName))).toBe(false); - }); - - it("preserves existing directory contents when validation fails even with --force", () => { - const projectName = "force-validation-failure"; - const projectDir = join(testDir, projectName); - mkdirSync(projectDir, { recursive: true }); - writeFileSync(join(projectDir, "README.md"), "existing content"); - - const result = runInitExpectFailure([ - projectName, - "--non-interactive", - "--appType", - "webviewer", - "--force", - "--layoutName", - "Contacts", - "--schemaName", - "Contacts", - "--noInstall", - "--noGit", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("FileMaker flags require --dataSource filemaker in non-interactive mode."); - expect(readFileSync(join(projectDir, "README.md"), "utf-8")).toBe("existing content"); - expect(existsSync(join(projectDir, "package.json"))).toBe(false); - expect(existsSync(join(projectDir, "proofkit.json"))).toBe(false); - }); - - it("does not surface typegen guidance for browser scaffolds without a typegen script", () => { - const projectName = "browser-no-fm-guidance"; - const output = runInitExpectSuccess([ - projectName, - "--non-interactive", - "--appType", - "browser", - "--dataSource", - "none", - "--noInstall", - "--noGit", - ]); - - const packageJson = JSON.parse(readFileSync(join(testDir, projectName, "package.json"), "utf-8")) as { - scripts?: Record; - }; - expect(packageJson.scripts?.typegen).toBeUndefined(); - expect(output).not.toMatch(typegenCommandPattern); - }); -}); diff --git a/packages/cli-old/tests/init-post-init-generation-errors.test.ts b/packages/cli-old/tests/init-post-init-generation-errors.test.ts deleted file mode 100644 index f13e9dff..00000000 --- a/packages/cli-old/tests/init-post-init-generation-errors.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import { createPostInitGenerationError, isMissingTypegenCommandError } from "~/cli/init"; - -vi.mock("@inquirer/prompts", () => ({ - checkbox: vi.fn(), - confirm: vi.fn(), - input: vi.fn(), - password: vi.fn(), - search: vi.fn(), - select: vi.fn(), -})); - -describe("init post-init generation error handling", () => { - it("detects missing typegen command failures", () => { - const commandError = new Error( - 'Command failed with exit code 254: pnpm typegen\nERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "typegen" not found', - ); - - expect(isMissingTypegenCommandError(commandError)).toBe(true); - }); - - it("does not classify broad pnpm typegen execution failures as missing command", () => { - const commandError = new Error( - "Command failed with exit code 1: pnpm typegen\nError: connect ECONNREFUSED 127.0.0.1:3000", - ); - - expect(isMissingTypegenCommandError(commandError)).toBe(false); - }); - - it("creates browser-specific guidance for missing typegen command failures", () => { - const commandError = new Error( - 'Command failed with exit code 254: pnpm typegen\nERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "typegen" not found', - ); - - const userFacingError = createPostInitGenerationError({ - error: commandError, - appType: "browser", - projectDir: "/tmp/demo-browser", - }); - - expect(userFacingError.message).toContain("Post-init generation failed after scaffolding."); - expect(userFacingError.message).toContain("Project created at: /tmp/demo-browser"); - expect(userFacingError.message).toContain("browser scaffolds do not define that script"); - expect(userFacingError.message).toContain("proofkit typegen"); - }); - - it("creates generic recovery guidance for other generation failures", () => { - const commandError = new Error("Unable to read layout metadata"); - - const userFacingError = createPostInitGenerationError({ - error: commandError, - appType: "webviewer", - projectDir: "/tmp/demo-webviewer", - }); - - expect(userFacingError.message).toContain("Post-init generation failed after scaffolding."); - expect(userFacingError.message).toContain("Project created at: /tmp/demo-webviewer"); - expect(userFacingError.message).toContain("Retry `proofkit typegen`"); - expect(userFacingError.message).toContain("Underlying error: Unable to read layout metadata"); - }); -}); diff --git a/packages/cli-old/tests/init-run-init-regression.test.ts b/packages/cli-old/tests/init-run-init-regression.test.ts deleted file mode 100644 index 6422d97b..00000000 --- a/packages/cli-old/tests/init-run-init-regression.test.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const { - createBareProjectMock, - setImportAliasMock, - promptForFileMakerDataSourceMock, - runCodegenCommandMock, - initializeGitMock, - logNextStepsMock, - readJSONSyncMock, - writeJSONSyncMock, - execaMock, - mockState, -} = vi.hoisted(() => ({ - createBareProjectMock: vi.fn(), - setImportAliasMock: vi.fn(), - promptForFileMakerDataSourceMock: vi.fn(), - runCodegenCommandMock: vi.fn(), - initializeGitMock: vi.fn(), - logNextStepsMock: vi.fn(), - readJSONSyncMock: vi.fn(), - writeJSONSyncMock: vi.fn(), - execaMock: vi.fn(), - mockState: { - appType: undefined as "browser" | "webviewer" | undefined, - ui: "shadcn" as "shadcn" | "mantine", - projectDir: "/tmp/proofkit-regression", - }, -})); - -vi.mock("@clack/prompts", () => ({ - intro: vi.fn(), - outro: vi.fn(), - note: vi.fn(), - cancel: vi.fn(), - log: { - error: vi.fn(), - info: vi.fn(), - message: vi.fn(), - step: vi.fn(), - success: vi.fn(), - warn: vi.fn(), - }, - spinner: vi.fn(() => ({ - message: vi.fn(), - start: vi.fn(), - stop: vi.fn(), - })), - isCancel: vi.fn(() => false), - select: vi.fn(), - text: vi.fn(), -})); - -vi.mock("@inquirer/prompts", () => ({ - checkbox: vi.fn(), - confirm: vi.fn(), - input: vi.fn(), - password: vi.fn(), - search: vi.fn(), - select: vi.fn(), -})); - -vi.mock("fs-extra", () => ({ - default: { - readJSONSync: readJSONSyncMock, - writeJSONSync: writeJSONSyncMock, - }, -})); - -vi.mock("execa", () => ({ - execa: execaMock, -})); - -vi.mock("~/helpers/createProject.js", () => ({ - createBareProject: createBareProjectMock, -})); - -vi.mock("~/helpers/setImportAlias.js", () => ({ - setImportAlias: setImportAliasMock, -})); - -vi.mock("~/cli/add/data-source/filemaker.js", () => ({ - promptForFileMakerDataSource: promptForFileMakerDataSourceMock, -})); - -vi.mock("~/generators/fmdapi.js", () => ({ - runCodegenCommand: runCodegenCommandMock, -})); - -vi.mock("~/helpers/git.js", () => ({ - initializeGit: initializeGitMock, -})); - -vi.mock("~/helpers/logNextSteps.js", () => ({ - logNextSteps: logNextStepsMock, -})); - -vi.mock("~/helpers/installDependencies.js", () => ({ - installDependencies: vi.fn(), -})); - -vi.mock("~/generators/auth.js", () => ({ - addAuth: vi.fn(), -})); - -vi.mock("~/installers/index.js", () => ({ - buildPkgInstallerMap: vi.fn(() => ({})), -})); - -vi.mock("~/state.js", () => ({ - state: mockState, - initProgramState: vi.fn(), - isNonInteractiveMode: vi.fn(() => true), -})); - -vi.mock("~/utils/getProofKitVersion.js", () => ({ - getVersion: vi.fn(() => "0.0.0-test"), -})); - -vi.mock("~/utils/getUserPkgManager.js", () => ({ - getUserPkgManager: vi.fn(() => "pnpm"), -})); - -vi.mock("~/utils/parseNameAndPath.js", () => ({ - parseNameAndPath: vi.fn((name: string) => [name, name]), -})); - -vi.mock("~/utils/parseSettings.js", () => ({ - setSettings: vi.fn(), -})); - -vi.mock("~/utils/validateAppName.js", () => ({ - validateAppName: vi.fn(() => undefined), -})); - -vi.mock("~/cli/utils.js", () => ({ - abortIfCancel: vi.fn((value: unknown) => value), -})); - -import { runInit } from "~/cli/init"; - -const browserFilemakerFlags = { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - server: undefined, - adminApiKey: undefined, - fileName: "", - layoutName: "", - schemaName: "", - dataApiKey: "", - fmServerURL: "", - auth: "none" as const, - dataSource: "filemaker" as const, - ui: "shadcn" as const, - CI: false, - nonInteractive: true, - tailwind: false, - trpc: false, - prisma: false, - drizzle: false, - appRouter: false, -}; - -describe("runInit browser post-init typegen regression", () => { - beforeEach(() => { - vi.clearAllMocks(); - - mockState.appType = undefined; - mockState.ui = "shadcn"; - mockState.projectDir = "/tmp/proofkit-regression"; - - createBareProjectMock.mockResolvedValue("/tmp/proofkit-regression/demo-browser"); - readJSONSyncMock.mockReturnValue({ name: "placeholder-app" }); - execaMock.mockResolvedValue({ stdout: "9.0.0" }); - promptForFileMakerDataSourceMock.mockResolvedValue(undefined); - - runCodegenCommandMock.mockRejectedValue( - new Error( - 'Command failed with exit code 254: pnpm typegen\nERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "typegen" not found', - ), - ); - }); - - it("does not run initial codegen for browser scaffolds after filemaker setup", async () => { - await expect(runInit("demo-browser", browserFilemakerFlags)).resolves.toBeUndefined(); - - expect(promptForFileMakerDataSourceMock).toHaveBeenCalledWith( - expect.objectContaining({ - projectDir: "/tmp/proofkit-regression/demo-browser", - }), - ); - expect(runCodegenCommandMock).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/cli-old/tests/init-scaffold-contract.test.ts b/packages/cli-old/tests/init-scaffold-contract.test.ts deleted file mode 100644 index 8d8a7332..00000000 --- a/packages/cli-old/tests/init-scaffold-contract.test.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; -import { join } from "node:path"; -import { parse as parseJsonc } from "jsonc-parser"; -import { beforeEach, describe, expect, it } from "vitest"; - -interface PackageJsonShape { - version?: string; - name?: string; - packageManager?: string; - scripts?: Record; - dependencies?: Record; - devDependencies?: Record; - proofkitMetadata?: { - initVersion?: string; - }; -} - -interface ProofkitSettings { - appType?: string; - ui?: string; - envFile?: string; - dataSources?: unknown[]; -} - -const cliPath = join(__dirname, "..", "dist", "index.js"); -const testDir = join(__dirname, "..", "..", "tmp", "cli-contract-tests"); -const browserProjectName = "contract-browser-project"; -const webviewerProjectName = "contract-webviewer-project"; -const browserProjectDir = join(testDir, browserProjectName); -const webviewerProjectDir = join(testDir, webviewerProjectName); -const cliPackageJsonPath = join(__dirname, "..", "package.json"); -const cliPackageJson = readJsonFile(cliPackageJsonPath); -const cliVersion = cliPackageJson.version ?? ""; -const expectedProofkitTag = cliVersion.includes("-") ? "beta" : "latest"; -const packageManagerPattern = /^(npm|pnpm|yarn|bun)@/; -const ansiStylePrefixPattern = /^[0-9;]*m/; - -function runInit({ appType, projectName }: { appType: "browser" | "webviewer"; projectName: string }): string { - return execFileSync( - "node", - [ - cliPath, - "init", - projectName, - "--non-interactive", - "--appType", - appType, - "--dataSource", - "none", - "--noGit", - "--noInstall", - ], - { - cwd: testDir, - env: process.env, - encoding: "utf-8", - stdio: "pipe", - }, - ); -} - -function readJsonFile(filePath: string): T { - return JSON.parse(readFileSync(filePath, "utf-8")) as T; -} - -function getProofkitDependencyVersions(pkg: PackageJsonShape): string[] { - const combined = { - ...(pkg.dependencies ?? {}), - ...(pkg.devDependencies ?? {}), - }; - - return Object.entries(combined) - .filter(([name]) => name.startsWith("@proofkit/")) - .map(([, version]) => version); -} - -function allProofkitDependenciesUseCurrentReleaseTag(pkg: PackageJsonShape): boolean { - const versions = getProofkitDependencyVersions(pkg); - return versions.length > 0 && versions.every((version) => version === expectedProofkitTag); -} - -function checkNodeSyntax(projectDir: string, relativeFilePath: string): boolean { - try { - execFileSync("node", ["--check", relativeFilePath], { - cwd: projectDir, - env: process.env, - encoding: "utf-8", - stdio: "pipe", - }); - - return true; - } catch { - return false; - } -} - -function getPackageManagerName(packageJson: PackageJsonShape): "npm" | "pnpm" | "yarn" | "bun" { - const raw = packageJson.packageManager?.split("@")[0]; - if (raw === "pnpm" || raw === "yarn" || raw === "bun") { - return raw; - } - return "npm"; -} - -function formatRunCommand(pkgManager: "npm" | "pnpm" | "yarn" | "bun", command: string): string { - return pkgManager === "npm" || pkgManager === "bun" ? `${pkgManager} run ${command}` : `${pkgManager} ${command}`; -} - -function sanitizeOutput(output: string): string { - return output - .split("\u001b[") - .map((segment, index) => (index === 0 ? segment : segment.replace(ansiStylePrefixPattern, ""))) - .join(""); -} - -function outputSuggestsCommand(output: string, command: string): boolean { - return output.includes(` ${command}`); -} - -describe("Init scaffold contract tests", () => { - beforeEach(() => { - rmSync(testDir, { recursive: true, force: true }); - mkdirSync(testDir, { recursive: true }); - }); - - it("creates deterministic browser scaffold output in non-interactive mode", () => { - const initOutput = runInit({ - appType: "browser", - projectName: browserProjectName, - }); - const normalizedOutput = sanitizeOutput(initOutput); - - expect(existsSync(browserProjectDir)).toBe(true); - expect(existsSync(join(browserProjectDir, "package.json"))).toBe(true); - expect(existsSync(join(browserProjectDir, "proofkit.json"))).toBe(true); - expect(existsSync(join(browserProjectDir, ".env"))).toBe(true); - expect(existsSync(join(browserProjectDir, "src", "lib", "env.ts"))).toBe(true); - expect(existsSync(join(browserProjectDir, "src", "app", "layout.tsx"))).toBe(true); - expect(existsSync(join(browserProjectDir, "postcss.config.mjs"))).toBe(true); - - const packageJson = readJsonFile(join(browserProjectDir, "package.json")); - expect(packageJson.name).toBe(browserProjectName); - expect(packageJson.scripts?.dev).toBe("next dev --turbopack"); - expect(packageJson.scripts?.build).toBe("next build --turbopack"); - expect(packageJson.scripts?.proofkit).toBe("proofkit"); - expect(packageJson.proofkitMetadata?.initVersion).toBe(cliVersion); - expect(packageJson.packageManager).toMatch(packageManagerPattern); - expect(allProofkitDependenciesUseCurrentReleaseTag(packageJson)).toBe(true); - const pkgManager = getPackageManagerName(packageJson); - expect(outputSuggestsCommand(normalizedOutput, formatRunCommand(pkgManager, "typegen"))).toBe(false); - - const proofkitConfig = readJsonFile(join(browserProjectDir, "proofkit.json")); - expect(proofkitConfig.appType).toBe("browser"); - expect(proofkitConfig.ui).toBe("shadcn"); - expect(proofkitConfig.envFile).toBe(".env"); - expect(proofkitConfig.dataSources).toEqual([]); - - // Compile-equivalent smoke check without external installs. - expect(checkNodeSyntax(browserProjectDir, "postcss.config.mjs")).toBe(true); - }); - - it("creates deterministic webviewer scaffold output in non-interactive mode", () => { - const initOutput = runInit({ - appType: "webviewer", - projectName: webviewerProjectName, - }); - const normalizedOutput = sanitizeOutput(initOutput); - - expect(existsSync(webviewerProjectDir)).toBe(true); - expect(existsSync(join(webviewerProjectDir, "package.json"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "proofkit.json"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "proofkit-typegen.config.jsonc"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, ".env"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "src", "main.tsx"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "scripts", "launch-fm.js"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "scripts", "upload.js"))).toBe(true); - - const packageJson = readJsonFile(join(webviewerProjectDir, "package.json")); - expect(packageJson.name).toBe(webviewerProjectName); - expect(packageJson.scripts?.build).toBe("vite build"); - expect(packageJson.scripts?.typegen).toBe("typegen"); - expect(packageJson.scripts?.["typegen:ui"]).toBe("typegen ui"); - expect(packageJson.scripts?.proofkit).toBe("proofkit"); - expect(packageJson.proofkitMetadata?.initVersion).toBe(cliVersion); - expect(packageJson.packageManager).toMatch(packageManagerPattern); - expect(allProofkitDependenciesUseCurrentReleaseTag(packageJson)).toBe(true); - const pkgManager = getPackageManagerName(packageJson); - expect(outputSuggestsCommand(normalizedOutput, formatRunCommand(pkgManager, "typegen"))).toBe(true); - expect(outputSuggestsCommand(normalizedOutput, formatRunCommand(pkgManager, "launch-fm"))).toBe(true); - - const proofkitConfig = readJsonFile(join(webviewerProjectDir, "proofkit.json")); - expect(proofkitConfig.appType).toBe("webviewer"); - expect(proofkitConfig.ui).toBe("shadcn"); - expect(proofkitConfig.envFile).toBe(".env"); - expect(proofkitConfig.dataSources).toEqual([]); - - const typegenConfigText = readFileSync(join(webviewerProjectDir, "proofkit-typegen.config.jsonc"), "utf-8"); - const typegenConfig = parseJsonc(typegenConfigText) as { - config?: { - type?: string; - path?: string; - validator?: string; - webviewerScriptName?: string; - fmMcp?: { - enabled?: boolean; - }; - }; - }; - expect(typegenConfig.config?.type).toBe("fmdapi"); - expect(typegenConfig.config?.path).toBe("./src/config/schemas/filemaker"); - expect(typegenConfig.config?.validator).toBe("zod/v4"); - expect(typegenConfig.config?.webviewerScriptName).toBe("ExecuteDataApi"); - expect(typegenConfig.config?.fmMcp?.enabled).toBe(true); - - // Compile-equivalent smoke checks without external installs. - expect(checkNodeSyntax(webviewerProjectDir, "scripts/launch-fm.js")).toBe(true); - expect(checkNodeSyntax(webviewerProjectDir, "scripts/upload.js")).toBe(true); - }); -}); diff --git a/packages/cli-old/tests/setup.ts b/packages/cli-old/tests/setup.ts deleted file mode 100644 index 9f7ba3cd..00000000 --- a/packages/cli-old/tests/setup.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { execSync } from "node:child_process"; -import path, { join } from "node:path"; -import dotenv from "dotenv"; -import { beforeAll } from "vitest"; - -beforeAll(() => { - // Ensure test environment variables are loaded - dotenv.config({ path: path.resolve(__dirname, "../.env.test") }); - process.env.PROOFKIT_SKIP_VERSION_CHECK = "1"; -}); - -// Build the CLI before running any tests -execSync("pnpm build", { cwd: join(__dirname, "..") }); diff --git a/packages/cli-old/tests/test-utils.ts b/packages/cli-old/tests/test-utils.ts deleted file mode 100644 index 7c8ce0b9..00000000 --- a/packages/cli-old/tests/test-utils.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { execSync } from "node:child_process"; -import { readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; - -/** - * Smoke-test helper only: swap workspace refs to published tags so install/build - * validates what end users can actually fetch from the registry. - */ -function usePublishedProofkitVersionsForSmoke(projectDir: string): void { - const pkgPath = join(projectDir, "package.json"); - const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); - - const replaceProofkitVersions = (deps: Record | undefined) => { - if (!deps) { - return; - } - for (const name of Object.keys(deps)) { - if (name.startsWith("@proofkit/")) { - console.log(` Replacing ${name}@${deps[name]} with latest`); - deps[name] = "latest"; - } - } - }; - - console.log("Using latest published @proofkit/* versions..."); - replaceProofkitVersions(pkg.dependencies); - replaceProofkitVersions(pkg.devDependencies); - - writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)); -} - -/** - * Verifies that a project at the given directory can be built without errors - * @param projectDir The directory containing the project to build - * @throws If the build fails - */ -export function verifySmokeProjectBuilds(projectDir: string): void { - console.log(`\nVerifying project build in ${projectDir}...`); - - try { - // Smoke tests intentionally validate published package installability. - usePublishedProofkitVersionsForSmoke(projectDir); - - console.log("Installing dependencies..."); - // Run pnpm install while ignoring workspace settings - execSync("pnpm install --prefer-offline --ignore-workspace", { - cwd: projectDir, - stdio: "inherit", - encoding: "utf-8", - env: { - ...process.env, - PNPM_DEBUG: "1", // Enable debug logging - }, - }); - - console.log("Building project..."); - execSync("pnpm build", { - cwd: projectDir, - stdio: "inherit", - encoding: "utf-8", - env: { - ...process.env, - NEXT_TELEMETRY_DISABLED: "1", - }, - }); - } catch (error) { - console.error("Build process failed:", error); - throw error; - } -} diff --git a/packages/cli-old/tests/webviewer-apps.test.ts b/packages/cli-old/tests/webviewer-apps.test.ts deleted file mode 100644 index 0f757955..00000000 --- a/packages/cli-old/tests/webviewer-apps.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { execSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import { beforeEach, describe, expect, it } from "vitest"; - -const nonInteractiveDirectoryError = /already exists and isn't empty/; - -describe("Web Viewer CLI Tests", () => { - const testDir = join(__dirname, "..", "..", "tmp", "cli-tests"); - const cliPath = join(__dirname, "..", "dist", "index.js"); - const projectName = "test-webviewer-project"; - const projectDir = join(testDir, projectName); - - beforeEach(() => { - if (existsSync(projectDir)) { - rmSync(projectDir, { recursive: true, force: true }); - } - mkdirSync(testDir, { recursive: true }); - }); - - it("should create a webviewer project without FileMaker server setup", () => { - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--appType webviewer", - "--noGit", - "--noInstall", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - }); - }).not.toThrow(); - - expect(existsSync(projectDir)).toBe(true); - expect(existsSync(join(projectDir, "package.json"))).toBe(true); - expect(existsSync(join(projectDir, "proofkit.json"))).toBe(true); - expect(existsSync(join(projectDir, "proofkit-typegen.config.jsonc"))).toBe(true); - - const packageJson = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf-8")); - expect(packageJson.scripts.typegen).toBe("typegen"); - expect(packageJson.scripts["typegen:ui"]).toBe("typegen ui"); - expect(packageJson.devDependencies["@proofkit/typegen"]).toBe("beta"); - - const proofkitConfig = JSON.parse(readFileSync(join(projectDir, "proofkit.json"), "utf-8")); - expect(proofkitConfig.appType).toBe("webviewer"); - expect(proofkitConfig.dataSources).toEqual([]); - }); - - it("should allow agent-only folders in non-interactive mode", () => { - mkdirSync(projectDir, { recursive: true }); - mkdirSync(join(projectDir, ".cursor"), { recursive: true }); - writeFileSync(join(projectDir, ".cursor", "rules.mdc"), "placeholder"); - - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--appType webviewer", - "--noGit", - "--noInstall", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - }); - }).not.toThrow(); - - expect(existsSync(join(projectDir, "package.json"))).toBe(true); - expect(existsSync(join(projectDir, ".cursor"))).toBe(true); - expect(existsSync(join(projectDir, ".cursor", "rules"))).toBe(false); - }); - - it("should allow hidden files in non-interactive mode", () => { - mkdirSync(projectDir, { recursive: true }); - writeFileSync(join(projectDir, ".DS_Store"), "placeholder"); - - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--appType webviewer", - "--noGit", - "--noInstall", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - }); - }).not.toThrow(); - - expect(existsSync(join(projectDir, "package.json"))).toBe(true); - expect(existsSync(join(projectDir, ".DS_Store"))).toBe(true); - }); - - it("should fail in non-interactive mode when .gitignore already exists", () => { - mkdirSync(projectDir, { recursive: true }); - writeFileSync(join(projectDir, ".gitignore"), "node_modules/\n"); - - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--appType webviewer", - "--noGit", - "--noInstall", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - stdio: "pipe", - }); - }).toThrow(nonInteractiveDirectoryError); - - expect(existsSync(join(projectDir, "package.json"))).toBe(false); - }); - - it("should fail without prompting when a non-interactive target directory has real files", () => { - mkdirSync(projectDir, { recursive: true }); - writeFileSync(join(projectDir, "README.md"), "existing content"); - - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--appType webviewer", - "--noGit", - "--noInstall", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - stdio: "pipe", - }); - }).toThrow(nonInteractiveDirectoryError); - - expect(existsSync(join(projectDir, "package.json"))).toBe(false); - }); -}); diff --git a/packages/cli-old/tsconfig.json b/packages/cli-old/tsconfig.json deleted file mode 100644 index 5e73ded4..00000000 --- a/packages/cli-old/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "baseUrl": "./", - "paths": { - "~/*": ["./src/*"], - "@config/*": ["../config/*"] - }, - "checkJs": true, - "strictNullChecks": true - }, - "exclude": ["template"], - "include": ["src", "tsdown.config.ts", "../reset.d.ts", "index.d.ts"] -} diff --git a/packages/cli-old/tsdown.config.ts b/packages/cli-old/tsdown.config.ts deleted file mode 100644 index 6813e73c..00000000 --- a/packages/cli-old/tsdown.config.ts +++ /dev/null @@ -1,54 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import replacePlugin from "@rollup/plugin-replace"; -import fsExtra from "fs-extra"; -import { defineConfig } from "tsdown"; - -const replace = replacePlugin.default ?? replacePlugin; - -const { readJSONSync } = fsExtra; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -const isDev = process.env.npm_lifecycle_event === "dev"; - -// Read package versions at build time -const readPackageVersion = (packagePath: string) => { - const packageJsonPath = path.join(__dirname, "..", packagePath, "package.json"); - const packageJson = readJSONSync(packageJsonPath); - if (!packageJson.version) { - throw new Error(`No version found in ${packageJsonPath}`); - } - return packageJson.version; -}; - -const FMDAPI_VERSION = readPackageVersion("fmdapi"); -const BETTER_AUTH_VERSION = readPackageVersion("better-auth"); -const WEBVIEWER_VERSION = readPackageVersion("webviewer"); -const TYPEGEN_VERSION = readPackageVersion("typegen"); - -export default defineConfig({ - clean: true, - entry: ["src/index.ts"], - format: ["esm"], - minify: !isDev, - target: "esnext", - outDir: "dist", - // Bundle workspace dependencies that shouldn't be external - noExternal: ["@proofkit/registry"], - // Keep Node.js built-in module imports as-is for better compatibility - nodeProtocol: false, - // Inject package versions and registry URL at build time - plugins: [ - replace({ - preventAssignment: true, - values: { - __FMDAPI_VERSION__: JSON.stringify(FMDAPI_VERSION), - __BETTER_AUTH_VERSION__: JSON.stringify(BETTER_AUTH_VERSION), - __WEBVIEWER_VERSION__: JSON.stringify(WEBVIEWER_VERSION), - __TYPEGEN_VERSION__: JSON.stringify(TYPEGEN_VERSION), - __REGISTRY_URL__: JSON.stringify(isDev ? "https://proofkit.localhost:1355" : "https://proofkit.proof.sh"), - }, - }), - ], - onSuccess: isDev ? "node dist/index.js" : undefined, -}); diff --git a/packages/cli-old/vitest.config.ts b/packages/cli-old/vitest.config.ts deleted file mode 100644 index c64f5913..00000000 --- a/packages/cli-old/vitest.config.ts +++ /dev/null @@ -1,24 +0,0 @@ -import path from "node:path"; -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - resolve: { - alias: { - "~": path.resolve(__dirname, "src"), - }, - }, - test: { - globals: true, - environment: "node", - setupFiles: ["./tests/setup.ts"], - include: ["tests/**/*.test.ts"], - // Deterministic contract/default tests only. - exclude: ["**/node_modules/**", "**/dist/**", "tests/**/*.smoke.test.ts"], - testTimeout: 60_000, // 60 seconds for CLI tests which can be slow - coverage: { - provider: "v8", - reporter: ["text", "json", "html"], - include: ["src/**/*.ts"], - }, - }, -}); diff --git a/packages/cli-old/vitest.smoke.config.ts b/packages/cli-old/vitest.smoke.config.ts deleted file mode 100644 index dae55ef3..00000000 --- a/packages/cli-old/vitest.smoke.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import path from "node:path"; -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - resolve: { - alias: { - "~": path.resolve(__dirname, "src"), - }, - }, - test: { - globals: true, - environment: "node", - setupFiles: ["./tests/setup.ts"], - include: ["tests/**/*.smoke.test.ts"], - exclude: ["**/node_modules/**", "**/dist/**"], - testTimeout: 60_000, - }, -}); diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md deleted file mode 100644 index 4f2c81a7..00000000 --- a/packages/cli/CHANGELOG.md +++ /dev/null @@ -1,571 +0,0 @@ -# @proofgeist/kit - -## 2.2.2 - -### Patch Changes - -- ff18231: Read ProofKit CLI version from package.json at build time. - -## 2.2.2-beta.0 - -### Patch Changes - -- ff18231: Read ProofKit CLI version from package.json at build time. - -## 2.2.1 - -### Patch Changes - -- Fix CLI version reporting in installer binary. The bundled proofkit binary now regenerates package-versions.ts before compilation, ensuring the correct version is baked in. - -## 2.2.0 - -### Minor Changes - -- 7d48139: Ship ProofKit CLI in installer and publish CLI packages. - -### Patch Changes - -- b1d4a68: Publish CLI beta releases to npm when Changesets is in beta pre mode. -- cd0f06f: Set installed proofkit launcher template root from app payload and remove redundant agent setup next step. -- 34b3c3c: Fix lint failures in freshly scaffolded projects: - - Stop writing a competing legacy `.oxlintrc.json` alongside Ultracite v7's scaffolded `oxlint.config.ts` for webviewer apps. The CLI now writes `oxlint.config.ts` for both browser and webviewer app types. - - Drop the `// @ts-nocheck` directive from the generated `oxlint.config.ts`. It is no longer needed (oxlint v1 ships proper types for `defineConfig`) and was itself failing Ultracite's `typescript/ban-ts-comment` rule. - - Remove unused `redirect` import from the `vite-wv` `router.tsx` template that was triggering an `eslint/no-unused-vars` error on first lint. - -- d58e177: Include initial props wiring in generated WebViewer apps. -- dbab7b3: Fix ultracite init during scaffold; now uses latest major version 7 -- 50dacdf: Add transient ProofKit token passthrough for local FileMaker MCP setup. -- a0604e8: Remove generated app dependency on `@proofkit/cli`, allow compatible package manager minor versions, and print recovery details when dependency install fails during init. -- deb859d: Install Bun before npm publish binary build. -- 2a11681: Raise generated app Node floor to avoid pnpm skipping native oxlint bindings. - -## 2.2.0-beta.6 - -### Patch Changes - -- 34b3c3c: Fix lint failures in freshly scaffolded projects: - - Stop writing a competing legacy `.oxlintrc.json` alongside Ultracite v7's scaffolded `oxlint.config.ts` for webviewer apps. The CLI now writes `oxlint.config.ts` for both browser and webviewer app types. - - Drop the `// @ts-nocheck` directive from the generated `oxlint.config.ts`. It is no longer needed (oxlint v1 ships proper types for `defineConfig`) and was itself failing Ultracite's `typescript/ban-ts-comment` rule. - - Remove unused `redirect` import from the `vite-wv` `router.tsx` template that was triggering an `eslint/no-unused-vars` error on first lint. - -## 2.2.0-beta.5 - -### Patch Changes - -- deb859d: Install Bun before npm publish binary build. -- 2a11681: Raise generated app Node floor to avoid pnpm skipping native oxlint bindings. - -## 2.2.0-beta.4 - -### Patch Changes - -- dbab7b3: Fix ultracite init during scaffold; now uses latest major version 7 - -## 2.2.0-beta.3 - -### Patch Changes - -- b1d4a68: Publish CLI beta releases to npm when Changesets is in beta pre mode. -- d58e177: Include initial props wiring in generated WebViewer apps. -- a0604e8: Remove generated app dependency on `@proofkit/cli`, allow compatible package manager minor versions, and print recovery details when dependency install fails during init. - -## 2.1.0-beta.1 - -### Patch Changes - -- cd0f06f: Set installed proofkit launcher template root from app payload and remove redundant agent setup next step. - -## 2.1.0-beta.0 - -### Minor Changes - -- 7d48139: Ship ProofKit CLI in installer and publish CLI packages. - -### Patch Changes - -- 50dacdf: Add transient ProofKit token passthrough for local FileMaker MCP setup. - -## 2.0.5 - -### Patch Changes - -- b80e426: Fix Next template GitHub icon import. -- b62a73c: Restrict Node engines to 22, 24, or 26. -- 5cd1375: Write npm min-release-age config during npm project scaffolding. -- b62a73c: Parse generated package-manager commands with shell-style quoting. -- ea479ed: Add oxlint to generated app dev dependencies. -- 5cd1375: Run Ultracite and TanStack Intent setup during project scaffolding. - -## 2.0.4 - -### Patch Changes - -- 64b43aa: Use package manager execute command in init next steps. -- f1dd2c5: Prefer pnpm when npm invokes scaffolding and warn npm fallback users to use pnpm 11+. -- e13c759: Use devEngines packageManager in generated apps. -- f1dd2c5: Use package-manager exec command in generated typegen scripts. - -## 2.0.3 - -### Patch Changes - -- 16a0542: Publish prebuilt CLI binaries and cut install-time runtime deps so `npx` and `pnpm dlx` flows avoid dependency build approvals. - -## 2.0.2 - -### Patch Changes - -- cbf7bc7: Fix pnpm 11 scaffold installs in release smoke tests, including browser app build-script policy generation. -- d2947e1: Add pnpm 11 build policy to generated WebViewer projects so fresh installs can complete without broad dependency script approval. - -## 2.0.1 - -### Patch Changes - -- b3f820a: Use caret versions for scaffolded ProofKit deps. -- f1869dd: Download FileMaker add-ons from CDN instead of bundled CLI templates. - -## 2.0.0 - -### Major Changes - -- d3c7979: ProofKit CLI v2 - - Rewrite the CLI internals on Effect for tagged errors, observability, composability, and cleaner top-level error output. User cancellations are unified and rendered consistently. - - Refocus the CLI around project bootstrap and diagnostics: new `doctor` and placeholder `prompt` commands, updated default guidance and docs, and scaffolded typegen scripts now use the package-native `@proofkit/typegen` commands. - - Default new projects to shadcn/ui. The legacy Mantine scaffold (`nextjs-mantine`) and the `--ui` init flag have been removed; existing Mantine projects are not actively supported. - - Limit `proofkit add` to supported ProofKit add-ons (the unused registry-backed install path is removed). To add new pages or auth, pass the component name (e.g. `proofkit add table/basic`). - -### Minor Changes - -- b73b0d7: Rebrand FM HTTP โ†’ FM MCP across the stack: adapter, config fields, and all references now use `fm-mcp` / `FmMcp` to reflect the FileMaker MCP server branding. Adds an optional `fmMcp` typegen config for using an FM MCP proxy during metadata fetching, revamps the Web Viewer Vite template, hardens `proofkit init` (ignores hidden files, improves non-interactive prompts, stops generating Cursor rules), installs typegen skills locally when scaffolding, and ships initial Codex skills for fmdapi/fmodata/webviewer. - -### Patch Changes - -- 41c07ba: Auto-detect non-interactive terminals for CLI commands in CI, scripted runs, and coding-agent environments. -- 03294e5: Init now writes `CLAUDE.md` as `@AGENTS.md` and adds `.cursorignore` to keep `CLAUDE.md` out of Cursor scans. -- bacdb7d: Make initial git commit failures non-blocking during CLI init. -- 8818805: Fix `proofkit add addon` so it works outside an existing ProofKit project. -- 63d309b: Fix browser FileMaker scaffolds to install `@proofkit/typegen` and run the local `typegen` bin during initial codegen. -- e6d0c55: Improve the local ProofKit MCP / FileMaker setup flow during webviewer init: - - Install the local addon files before prompting that no FileMaker file is open. - - After retry, report the connected FileMaker file and prompt to choose when multiple files are open. - - Require an explicit local FileMaker file selection in non-interactive multi-file setups, and persist the selection (or the lone connected file) into the generated `proofkit-typegen.config.jsonc` as `fmMcp.connectedFileName`. - - Normalize non-interactive FileMaker layout names against live layout casing so case-only drift doesn't break hosted init or release smoke tests. - - Clarify the wording of the local FileMaker / ProofKit plugin setup prompts. - -- d5ca0e5: Preserve typed cancellation errors in the default project menu and wrap add/remove menu failures with stable error messages. -- 9add5ca: Project name parsing for `proofkit init`: normalize whitespace to dashes, lowercase `.`-derived names from the current directory, clarify that `.` means the current directory, and preserve leading directory segments verbatim in `parseNameAndPath`. -- be34116: Make scaffolded Web Viewer upload scripts use `deploy_html` as the canonical FileMaker script, with bridge-first and FMP URL fallback deployment. -- d7f86a4: Update newly scaffolded apps to use Ultracite for linting and formatting by default, including the generated `lint` and `format` scripts and CLI formatting flow. -- e0ea042: Update bundled FileMaker addon to fix a bug in the SendCallback script. -- c71b0d4: Add `utils/fmdapi` helper to the ProofKit registry. -- Updated dependencies - - @proofkit/typegen@1.1.0 - - @proofkit/fmdapi@5.1.0 - -## 2.0.0-beta.34 - -### Patch Changes - -- bacdb7d: Make initial git commit failures non-blocking during CLI init. - -## 2.0.0-beta.33 - -### Patch Changes - -- ee4c951: Clarify local FileMaker setup prompts for the ProofKit plugin flow. -- d5ca0e5: Preserve typed cancellation errors in the default project menu and wrap add/remove menu failures with stable error messages. -- ee4c951: Restore the interactive project menu when running `proofkit` inside an existing ProofKit project. -- be34116: Make scaffolded Web Viewer upload scripts use `deploy_html` as the canonical FileMaker script, with bridge-first and FMP URL fallback deployment. - - @proofkit/typegen@1.1.0-beta.27 - -## 2.0.0-beta.32 - -### Patch Changes - -- 7c7f70a: swap docs domain to proofkit.proof.sh -- 18ade4d: Limit `proofkit add` to supported ProofKit add-ons and remove the unused registry-backed install path. -- Updated dependencies [7c7f70a] - - @proofkit/fmdapi@5.1.0-beta.5 - - @proofkit/typegen@1.1.0-beta.26 - -## 2.0.0-beta.31 - -### Patch Changes - -- Updated dependencies [c031d74] - - @proofkit/typegen@1.1.0-beta.25 - -## 2.0.0-beta.30 - -### Patch Changes - -- 63d309b: Fix browser FileMaker scaffolds to install `@proofkit/typegen` and run the local `typegen` bin during initial codegen. -- d8fba3f: Normalize non-interactive FileMaker layout names against live layout casing so hosted init and release smoke tests do not fail on case-only layout drift. -- Updated dependencies [2f0f8f3] - - @proofkit/typegen@1.1.0-beta.24 - -## 2.0.0-beta.29 - -### Patch Changes - -- c79f183: Fix FileMaker webviewer init flow to install local addon files before prompting that no FileMaker file is open in the local MCP server. - - @proofkit/typegen@1.1.0-beta.23 - -## 2.0.0-beta.28 - -### Patch Changes - -- 8818805: Fix `proofkit add addon` so it works outside an existing ProofKit project. -- e0ea042: updated addon to fix a bug in the SendCallback script -- Updated dependencies [0643ddd] -- Updated dependencies [e6889d0] - - @proofkit/typegen@1.1.0-beta.22 - - @proofkit/fmdapi@5.1.0-beta.4 - -## 2.0.0-beta.27 - -### Patch Changes - -- 5bc5504: Init(webviewer): if local FM MCP reports exactly 1 connected file, persist it to `proofkit-typegen.config.jsonc` as `fmMcp.connectedFileName` during scaffold. -- 03294e5: Init now writes `CLAUDE.md` as `@AGENTS.md` and adds `.cursorignore` to keep `CLAUDE.md` out of Cursor scans. -- 4f40bfe: Normalize and validate `.`-derived CLI project names from the current directory consistently, including whitespace-to-dash conversion and lowercasing -- db11fda: Normalize only the final path segment in `parseNameAndPath`, preserving leading directory segments verbatim while keeping scoped-name parsing and `.` handling intact -- fe43be6: Drop the unused `nextjs-mantine` scaffold from the current CLI and always scaffold browser apps from `nextjs-shadcn`. -- 9add5ca: Remove the `--ui` init flag. ProofKit now only scaffolds shadcn. -- 9add5ca: Allow spaces in project names by normalizing them to dashes -- 9add5ca: Clarify that `.` uses the current directory for `proofkit init` -- Updated dependencies [c85574f] -- Updated dependencies [6da0c9a] - - @proofkit/typegen@1.1.0-beta.21 - -## 2.0.0-beta.26 - -### Patch Changes - -- e3b25c3: Refocus the ProofKit CLI around project bootstrap and diagnostics by adding `doctor` and placeholder `prompt` commands, updating default guidance and docs, and switching scaffolded typegen scripts to package-native `@proofkit/typegen` commands. - -## 2.0.0-beta.25 - -### Patch Changes - -- 41c07ba: Auto-detect non-interactive terminals for CLI commands in CI, scripted runs, and coding-agent environments. -- 1096f3b: Improve `proofkit init` error handling by using tagged Effect-based CLI errors for expected failures, unifying user cancellation, and rendering cleaner top-level error output. -- e6d0c55: Improve local ProofKit MCP setup messaging during webviewer init by reporting the connected FileMaker file after retry and prompting to choose a file when multiple files are open. -- 46696e4: Require `proofkit init` to use an explicit local FileMaker file selection in non-interactive multi-file setups, and save the selected local file into the generated typegen config. -- Updated dependencies [7b46a23] -- Updated dependencies [88242c2] - - @proofkit/typegen@1.1.0-beta.20 - -## 2.0.0-beta.23 - -### Minor Changes - -- b73b0d7: - cli: Revamp the Web Viewer Vite template and harden `proofkit init` (ignore hidden files, improve non-interactive prompts, stop generating Cursor rules). - - cli: Install typegen skills locally when scaffolding projects. - - typegen: Add optional `fmMcp` config for using an FM MCP proxy during metadata fetching. - - fmdapi/fmodata/webviewer: Add initial Codex skills for client and integration workflows. -- b73b0d7: Rebrand FM HTTP โ†’ FM MCP across the stack. The adapter, config fields, and all references now use `fm-mcp` / `FmMcp` naming to reflect the FileMaker MCP server branding. - -### Patch Changes - -- Updated dependencies [b73b0d7] -- Updated dependencies [b73b0d7] - - @proofkit/typegen@1.1.0-beta.19 - - @proofkit/fmdapi@5.1.0-beta.3 - -## 2.0.0-beta.1 - -### Major Changes - -- d3c7979: Rewrite the CLI package for better observability, composability, and error tracing. - -### Patch Changes - -- d7f86a4: Update newly scaffolded apps to use Ultracite for linting and formatting by default, including the generated `lint` and `format` scripts and CLI formatting flow. - - @proofkit/typegen@1.1.0-beta.18 - -## 2.0.0-beta.22 - -### Minor Changes - -- 5544f68: - cli: Revamp the Web Viewer Vite template and harden `proofkit init` (ignore hidden files, improve non-interactive prompts, stop generating Cursor rules). - - cli: Install typegen skills locally when scaffolding projects. - - typegen: Add optional `fmHttp` config for using an FM HTTP proxy during metadata fetching. - - fmdapi/fmodata/webviewer: Add initial Codex skills for client and integration workflows. - -### Patch Changes - -- Updated dependencies [5544f68] -- Updated dependencies [f3980b1] -- Updated dependencies [8ca7a1e] -- Updated dependencies [1d4b69d] - - @proofkit/typegen@1.1.0-beta.17 - - @proofkit/fmdapi@5.1.0-beta.2 - -## 2.0.0-beta.21 - -### Patch Changes - -- Updated dependencies [2df365d] - - @proofkit/typegen@1.1.0-beta.16 - -## 2.0.0-beta.20 - -### Patch Changes - -- @proofkit/typegen@1.1.0-beta.15 - -## 2.0.0-beta.19 - -### Patch Changes - -- Updated dependencies [4e048d1] - - @proofkit/typegen@1.1.0-beta.14 - -## 2.0.0-beta.18 - -### Patch Changes - -- Updated dependencies [4928637] - - @proofkit/typegen@1.1.0-beta.13 - -## 2.0.0-beta.17 - -### Patch Changes - -- @proofkit/typegen@1.1.0-beta.12 - -## 2.0.0-beta.16 - -### Patch Changes - -- @proofkit/typegen@1.1.0-beta.11 - -## 2.0.0-beta.15 - -### Patch Changes - -- @proofkit/typegen@1.1.0-beta.10 - -## 2.0.0-beta.14 - -### Patch Changes - -- Updated dependencies [eb7d751] - - @proofkit/typegen@1.1.0-beta.9 - -## 2.0.0-beta.13 - -### Patch Changes - -- @proofkit/typegen@1.1.0-beta.8 - -## 2.0.0-beta.12 - -### Patch Changes - -- Updated dependencies [3b55d14] - - @proofkit/typegen@1.1.0-beta.7 - -## 2.0.0-beta.11 - -### Patch Changes - -- Updated dependencies - - @proofkit/typegen@1.1.0-beta.6 - -## 2.0.0-beta.10 - -### Patch Changes - -- Updated dependencies [ae07372] -- Updated dependencies [23639ec] -- Updated dependencies [dfe52a7] - - @proofkit/typegen@1.1.0-beta.5 - -## 2.0.0-beta.9 - -### Patch Changes - -- 863e1e8: Update tooling to Biome -- Updated dependencies [7dbfd63] -- Updated dependencies [863e1e8] - - @proofkit/typegen@1.1.0-beta.4 - - @proofkit/fmdapi@5.0.3-beta.1 - -## 2.0.0-beta.8 - -### Patch Changes - -- @proofkit/typegen@1.1.0-beta.3 - -## 2.0.0-beta.4 - -### Patch Changes - -- Updated dependencies [4d9d0e9] - - @proofkit/typegen@1.0.11-beta.1 - -## 1.1.8 - -### Patch Changes - -- 00177bf: Guard page add/remove against missing `src/app/navigation.tsx` so Web Viewer apps donโ€™t error when updating navigation. This safely no-ops when the navigation file isnโ€™t present. -- Updated dependencies [7c602a9] -- Updated dependencies [a29ca94] - - @proofkit/typegen@1.0.10 - - @proofkit/fmdapi@5.0.2 - -## 1.1.5 - -### Patch Changes - -- Run typegen code directly instead of via execa -- error trap around formatting -- Remove shared-utils dep - -## 1.1.0 - -### Minor Changes - -- 7429a1e: Add simultaneous support for Shadcn. New projects will have Shadcn initialized automatically, and the upgrade command will offer to automatically add support for Shadcn to an existing ProofKit project. - -### Patch Changes - -- b483d67: Update formatting after typegen to be more consistent -- f0ddde2: Upgrade next-safe-action to v8 (and related dependencies) -- 7c87649: Fix getFieldNamesForSchema function - -## 1.0.0 - -### Major Changes - -- c348e37: Support @proofkit namespaced packages - -### Patch Changes - -- Updated dependencies [16fb8bd] -- Updated dependencies [16fb8bd] -- Updated dependencies [16fb8bd] - - @proofkit/fmdapi@5.0.0 - -## 0.3.2 - -### Patch Changes - -- 8986819: Fix: name argument in add command optional -- 47aad62: Make the auth installer spinner good - -## 0.3.1 - -### Patch Changes - -- 467d0f9: Add new menu command to expose all proofkit functions more easily -- 6da944a: Ensure using authedActionClient in existing actions after adding auth -- b211fbd: Deploy command: run build on Vercel instead of locally. Use flag --local-build to build locally like before -- 39648a9: Fix: Webviewer addon installation flow -- d0627b2: update base package versions - -## 0.3.0 - -### Minor Changes - -- 846ae9a: Add new upgrade command to upgrade ProofKit components in an existing project. To start, this command only adds/updates the cursor rules in your project. - -### Patch Changes - -- e07341a: Always use accessorFn for tables for better type errors - -## 0.2.3 - -### Patch Changes - -- 217eb5b: Fixed infinite table queries for other field names -- 217eb5b: New infinite table editable template - -## 0.2.2 - -### Patch Changes - -- ffae753: Better https parsing when prompting for the FileMaker Server URL -- 415be19: Add options for password strength in fm-addon auth. Default to not check for compromised passwords -- af5feba: Fix the launch-fm script for web viewer - -## 0.2.1 - -### Patch Changes - -- 6e44193: update helper text for npm after adding page -- 6e44193: additional supression of hydration warning -- 6e44193: move question about adding data source for new project -- 183988b: fix import path for reset password helper -- 6e44193: Make an initial commit when initializing git repo -- e0682aa: Copy cursor rules.mdc file into the base project. - -## 0.2.0 - -### Minor Changes - -- 6073cfe: Allow deploying a demo file to your server instead of having to pick an existing file - -### Patch Changes - -- d0f5c6e: Fix: post-install template functions not running - -## 0.1.2 - -### Patch Changes - -- 92cb423: fix: runtime error due to external shared package - -## 0.1.1 - -### Patch Changes - -- f88583c: prompt user to login to Vercel if needed during deploy command - -## 0.1.0 - -### Minor Changes - -- c019363: Add Deploy command for Vercel - -### Patch Changes - -- 0b7bf78: Allow setup without any data sources - -## 0.0.15 - -### Patch Changes - -- 1ff4aa7: Hide options for unsupported features in webviewer apps -- 5cfd0aa: Add infinite table page template -- 063859a: Added Template: Editable Table -- de0c2ab: update shebang in index -- b7ad0cf: Stream output from the typegen command - -## 0.0.6 - -### Patch Changes - -- Adding pages - -## 0.0.3 - -### Patch Changes - -- add typegen command for fm - -## 0.0.2 - -### Patch Changes - -- fix auth in init - -## 0.0.2-beta.0 - -### Patch Changes - -- fix auth in init diff --git a/packages/cli/CLI_FLOW_AUDIT.md b/packages/cli/CLI_FLOW_AUDIT.md deleted file mode 100644 index 82e4bc31..00000000 --- a/packages/cli/CLI_FLOW_AUDIT.md +++ /dev/null @@ -1,111 +0,0 @@ -# ProofKit CLI Flow Audit - -## Actual runtime flow - -```mermaid -flowchart TD - A[proofkit] --> B{explicit subcommand?} - - B -->|no| C{proofkit.json in cwd?} - C -->|yes| D[print project guidance
doctor / prompt / init] - C -->|no + interactive| E[run init] - C -->|no + non-interactive| F[fail: explicit command required] - - B -->|init| G[Effect init flow] - G --> G1[resolve request] - G1 --> G2[plan init] - G2 --> G3[execute init plan] - - B -->|doctor| H[doctor audit] - B -->|prompt| I[placeholder note only] - - B -->|add| J{arg `name` present?} - J -->|addon| J1[add addon target] - J -->|tanstack-query| J2[run tanstack-query installer] - J -->|any other name| J3[installFromRegistry(name)] - J -->|no| J4{proofkit.json readable?} - J4 -->|no| J5[preflight add] - J5 --> J6[registry-driven add flow] - J4 -->|yes + ui=shadcn| J6 - J4 -->|yes + ui!=shadcn| J7[legacy interactive add menu] - J7 -->|page| J8[runAddPageAction] - J7 -->|schema| J9[runAddSchemaAction] - J7 -->|data| J10[runAddDataSourceCommand] - J7 -->|react-email| J11[runAddReactEmailCommand] - J7 -->|auth| J12[runAddAuthAction] - - B -->|remove| K[legacy interactive remove menu] - K -->|page| K1[runRemovePageAction] - K -->|schema| K2[runRemoveSchemaAction] - K -->|data| K3[runRemoveDataSourceCommand] - - B -->|typegen| L[legacy alias -> runCodegenCommand] - B -->|deploy| M[legacy deploy flow] - B -->|upgrade| N[legacy upgrade flow] -``` - -## Stranded legacy branches - -These paths still exist in legacy Commander modules, but new root routing does not expose them as subcommands: - -```mermaid -flowchart TD - A[legacy add Command] --> A1[add auth] - A --> A2[add addon] - A --> A3[add page] - A --> A4[add layout/schema] - A --> A5[add data] - - B[legacy remove Command] --> B1[remove page] - B --> B2[remove layout/schema] - B --> B3[remove data] - - C[legacy typegen Command] - D[legacy upgrade Command] -``` - -The new root CLI only exposes flat `add [name] [target]` and `remove [name]`, so those nested branches are currently implementation-only. - -## Gaps / dead ends - -1. `add` positional names misroute. - `runAdd()` special-cases only `addon` and `tanstack-query`, then sends every other provided `name` to `installFromRegistry(name)`. That means `proofkit add auth`, `proofkit add page`, `proofkit add schema`, `proofkit add layout`, `proofkit add data`, and `proofkit add react-email` do not hit their legacy handlers. They go to registry install instead. - -2. `remove [name]` is dead input. - `runRemove()` ignores `_name` entirely and always opens the interactive picker. So `proofkit remove page` does not remove a page directly. In non-interactive mode, this path has no direct branch and is likely unusable. - -3. Legacy subcommands still defined, but unreachable from root parser. - The root Effect CLI exposes `add` and `remove` as flat commands only. The nested Commander subcommands still exist under legacy `makeAddCommand()` and `makeRemoveCommand()`, but root help and parsing never surface `add auth`, `add page`, `remove page`, etc as true subcommands. - -4. `prompt` is a deliberate stub. - `proofkit prompt` exits successfully, but only prints a "coming soon" note. It is a real command but still a product dead end. - -5. Docs and runtime surface diverge. - Docs describe ProofKit as mainly `init`, `doctor`, and `prompt`, with package-native CLIs for ongoing work. Runtime still advertises `add`, `remove`, `typegen`, `deploy`, and `upgrade`. - -6. Naming drift: `schema` vs `layout`. - Legacy schema add/remove commands are actually named `layout` with alias `schema`. Interactive menus say "Schema". If nested subcommands come back, this naming split will still be confusing. - -## Tight fix list - -1. Pick one surface: flat verbs or nested subcommands. -2. If flat: - Make `runAdd(name)` dispatch explicit names to real handlers before registry fallback. -3. If flat: - Make `runRemove(name)` honor `page|schema|data`. -4. If nested: - Rebuild `add` and `remove` as Effect subcommand trees instead of flat arg parsers. -5. Hide or remove legacy commands still meant to be package-native only. -6. Either implement `prompt` or mark it hidden until ready. - -## Source refs - -- Root command surface: [packages/cli/src/index.ts](/Users/ericluce/Documents/Code/work/proofkit/packages/cli/src/index.ts:206) -- Root subcommand list: [packages/cli/src/index.ts](/Users/ericluce/Documents/Code/work/proofkit/packages/cli/src/index.ts:402) -- `add` dispatch: [packages/cli/src/cli/add/index.ts](/Users/ericluce/Documents/Code/work/proofkit/packages/cli/src/cli/add/index.ts:102) -- Stranded legacy `add` subcommands: [packages/cli/src/cli/add/index.ts](/Users/ericluce/Documents/Code/work/proofkit/packages/cli/src/cli/add/index.ts:166) -- `remove` ignoring arg: [packages/cli/src/cli/remove/index.ts](/Users/ericluce/Documents/Code/work/proofkit/packages/cli/src/cli/remove/index.ts:12) -- Stranded legacy `remove` subcommands: [packages/cli/src/cli/remove/index.ts](/Users/ericluce/Documents/Code/work/proofkit/packages/cli/src/cli/remove/index.ts:47) -- `layout` alias naming: [packages/cli/src/cli/add/fmschema.ts](/Users/ericluce/Documents/Code/work/proofkit/packages/cli/src/cli/add/fmschema.ts:193) -- `prompt` stub: [packages/cli/src/core/prompt.ts](/Users/ericluce/Documents/Code/work/proofkit/packages/cli/src/core/prompt.ts:5) -- Docs surface: [apps/docs/content/docs/cli/reference/cli-commands.mdx](/Users/ericluce/Documents/Code/work/proofkit/apps/docs/content/docs/cli/reference/cli-commands.mdx:12) diff --git a/packages/cli/README.md b/packages/cli/README.md deleted file mode 100644 index f8e1efec..00000000 --- a/packages/cli/README.md +++ /dev/null @@ -1,19 +0,0 @@ -

- - Logo for ProofKit - -

- -

- ProofKit CLI -

- -

- Interactive CLI to manage your TypeScript projects that connect with FileMaker -

- -

- Get started with a new ProofKit project by running pnpm create proofkit -

- -View full documentation at [proofkit.proof.sh](https://proofkit.proof.sh) diff --git a/packages/cli/bin/proofkit.cjs b/packages/cli/bin/proofkit.cjs deleted file mode 100644 index d83909a1..00000000 --- a/packages/cli/bin/proofkit.cjs +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env node -"use strict"; - -const { existsSync } = require("node:fs"); -const path = require("node:path"); -const { spawnSync } = require("node:child_process"); - -const BINARIES = { - darwin: { - arm64: "proofkit-darwin-arm64", - x64: "proofkit-darwin-x64", - }, - linux: { - arm64: "proofkit-linux-arm64", - x64: "proofkit-linux-x64", - }, - win32: { - arm64: "proofkit-windows-arm64.exe", - x64: "proofkit-windows-x64.exe", - }, -}; - -function run(command, args) { - const result = spawnSync(command, args, { - stdio: "inherit", - env: { - ...process.env, - PROOFKIT_PKG_ROOT: path.resolve(__dirname, ".."), - }, - }); - - if (result.error) { - throw result.error; - } - - if (typeof result.status === "number") { - process.exit(result.status); - } - - process.exit(1); -} - -if (process.env.PROOFKIT_DISABLE_BUNDLED_BINARY !== "1") { - const binaryName = BINARIES[process.platform]?.[process.arch]; - if (binaryName) { - const binaryPath = path.join(__dirname, binaryName); - if (existsSync(binaryPath)) { - run(binaryPath, process.argv.slice(2)); - } - } -} - -const fallbackPath = path.join(__dirname, "..", "dist", "index.js"); -if (existsSync(fallbackPath)) { - run(process.execPath, [fallbackPath, ...process.argv.slice(2)]); -} - -console.error( - `No ProofKit executable found for ${process.platform}-${process.arch}.`, -); -process.exit(1); diff --git a/packages/cli/index.d.ts b/packages/cli/index.d.ts deleted file mode 100644 index 9c577672..00000000 --- a/packages/cli/index.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface RouteLink { - label: string; - type: "link"; - href: string; - icon?: React.ReactNode; - /** If true, the route will only be considered active if the path is exactly this value. */ - exactMatch?: boolean; -} - -export interface RouteFunction { - label: string; - type: "function"; - icon?: React.ReactNode; - onClick: () => void; - /** If true, the route will only be considered active if the path is exactly this value. */ - exactMatch?: boolean; -} - -export type ProofKitRoute = RouteLink | RouteFunction; diff --git a/packages/cli/package.json b/packages/cli/package.json deleted file mode 100644 index dfc38be8..00000000 --- a/packages/cli/package.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "name": "@proofkit/cli", - "version": "2.2.2", - "description": "Interactive CLI to scaffold and manage ProofKit projects", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/proofsh/proofkit.git", - "directory": "packages/cli" - }, - "keywords": [ - "proofkit", - "filemaker", - "ottomatic", - "proofgeist", - "proofsh", - "next.js", - "typescript" - ], - "type": "module", - "exports": { - ".": { - "types": "./index.d.ts", - "import": "./dist/index.js" - } - }, - "bin": { - "proofkit": "bin/proofkit.cjs" - }, - "files": [ - "bin/proofkit.cjs", - "bin/proofkit-*", - "bin/*.exe", - "dist/**/*.js", - "dist/**/*.d.ts", - "template", - "README.md", - "CHANGELOG.md", - "index.d.ts", - "package.json" - ], - "engines": { - "node": "^22.12.0 || ^24.0.0" - }, - "scripts": { - "typecheck": "node ./scripts/write-cli-version.mjs && tsc --noEmit", - "build": "node ./scripts/write-cli-version.mjs && NODE_ENV=production tsdown && publint --strict", - "build:binaries": "node ./scripts/write-cli-version.mjs && node ./scripts/build-binaries.mjs", - "prepublishOnly": "pnpm build && pnpm build:binaries", - "dev": "tsdown --watch", - "clean": "rm -rf dist .turbo node_modules bin/proofkit-* bin/*.exe", - "start": "node dist/index.js", - "lint": "cd ../.. && pnpm exec ultracite check packages/cli/src packages/cli/tests packages/cli/package.json packages/cli/tsconfig.json packages/cli/tsdown.config.ts packages/cli/vitest.config.ts", - "lint:summary": "pnpm run lint", - "release": "changeset version", - "pub:beta": "NODE_ENV=production pnpm build && pnpm --package npm@^11 dlx npm publish --tag beta --access public", - "pub:next": "NODE_ENV=production pnpm build && pnpm --package npm@^11 dlx npm publish --tag next --access public", - "pub:release": "NODE_ENV=production pnpm build && pnpm --package npm@^11 dlx npm publish --access public", - "test": "pnpm build && node ./scripts/build-current-binary.mjs && PROOFKIT_DISABLE_BUNDLED_BINARY=1 vitest run", - "test:smoke": "PROOFKIT_RUN_SMOKE_TESTS=1 vitest run --config vitest.smoke.config.ts" - }, - "devDependencies": { - "@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/better-auth": "workspace:*", - "@proofkit/fmdapi": "workspace:*", - "@proofkit/typegen": "workspace:*", - "@proofkit/webviewer": "workspace:*", - "@types/glob": "^8.1.0", - "axios": "^1.13.2", - "chalk": "5.4.1", - "commander": "^14.0.2", - "dotenv": "^16.6.1", - "effect": "^3.20.0", - "es-toolkit": "^1.43.0", - "execa": "^9.6.1", - "fast-glob": "^3.3.3", - "fs-extra": "^11.3.3", - "glob": "^11.1.0", - "gradient-string": "^2.0.2", - "handlebars": "^4.7.8", - "jiti": "^1.21.7", - "jsonc-parser": "^3.3.1", - "open": "^10.2.0", - "ora": "6.3.1", - "randomstring": "^1.3.1", - "semver": "^7.7.3", - "shadcn": "^2.10.0", - "ts-morph": "^26.0.0", - "type-fest": "^3.13.1", - "@auth/drizzle-adapter": "^1.11.1", - "@auth/prisma-adapter": "^1.6.0", - "@libsql/client": "^0.6.2", - "@planetscale/database": "^1.19.0", - "@prisma/adapter-planetscale": "^5.22.0", - "@prisma/client": "^5.22.0", - "@rollup/plugin-replace": "^6.0.3", - "@t3-oss/env-nextjs": "^0.10.1", - "@tanstack/react-query": "^5.90.16", - "@trpc/client": "11.0.0-rc.441", - "@trpc/next": "11.0.0-rc.441", - "@trpc/react-query": "11.0.0-rc.441", - "@trpc/server": "11.0.0-rc.441", - "@types/axios": "^0.14.4", - "@types/fs-extra": "^11.0.4", - "@types/gradient-string": "^1.1.6", - "@types/node": "^22.19.5", - "@types/randomstring": "^1.3.0", - "@types/react": "19.2.7", - "@types/semver": "^7.7.1", - "@vitest/coverage-v8": "^2.1.9", - "drizzle-kit": "^0.21.4", - "drizzle-orm": "^0.30.10", - "mysql2": "^3.16.0", - "next": "16.1.1", - "next-auth": "^4.24.13", - "postgres": "^3.4.8", - "prisma": "^5.22.0", - "publint": "^0.3.16", - "react": "19.2.3", - "react-dom": "19.2.3", - "superjson": "^2.2.6", - "tailwindcss": "^4.1.18", - "tsdown": "^0.14.2", - "typescript": "^5.9.3", - "vitest": "^4.0.17", - "zod": "^4.3.5" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/cli/scripts/build-binaries.mjs b/packages/cli/scripts/build-binaries.mjs deleted file mode 100644 index ecce84cd..00000000 --- a/packages/cli/scripts/build-binaries.mjs +++ /dev/null @@ -1,101 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { chmodSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const packageRoot = path.resolve(__dirname, ".."); -const binDir = path.join(packageRoot, "bin"); -const entrypoint = path.join(packageRoot, "src", "index.ts"); - -const targets = [ - { target: "bun-darwin-arm64", file: "proofkit-darwin-arm64" }, - { target: "bun-darwin-x64", file: "proofkit-darwin-x64" }, - { target: "bun-linux-arm64", file: "proofkit-linux-arm64" }, - { target: "bun-linux-x64", file: "proofkit-linux-x64" }, - { target: "bun-windows-arm64", file: "proofkit-windows-arm64.exe" }, - { target: "bun-windows-x64", file: "proofkit-windows-x64.exe" }, -]; -const validTargets = new Set(targets.map((config) => config.target)); -const requestedTargetsEnv = process.env.PROOFKIT_BINARY_TARGETS ?? ""; - -const selectedTargets = new Set( - requestedTargetsEnv - .split(",") - .map((target) => target.trim()) - .filter(Boolean), -); -const filteredSelectedTargets = new Set( - [...selectedTargets].filter((target) => validTargets.has(target)), -); - -if (selectedTargets.size > 0 && filteredSelectedTargets.size === 0) { - console.error( - `No valid binary targets in PROOFKIT_BINARY_TARGETS="${requestedTargetsEnv}". Valid targets: ${targets - .map((config) => config.target) - .join(", ")}`, - ); - process.exit(1); -} - -mkdirSync(binDir, { recursive: true }); -for (const file of readdirSync(binDir)) { - if (file === "proofkit.cjs") { - continue; - } - rmSync(path.join(binDir, file), { recursive: true, force: true }); -} - -let builtCount = 0; -for (const config of targets) { - if ( - filteredSelectedTargets.size > 0 && - !filteredSelectedTargets.has(config.target) - ) { - continue; - } - - const outfile = path.join(binDir, config.file); - const result = spawnSync( - "bun", - [ - "build", - "--compile", - `--target=${config.target}`, - "--no-compile-autoload-dotenv", - "--no-compile-autoload-bunfig", - "--no-compile-autoload-tsconfig", - "--no-compile-autoload-package-json", - entrypoint, - `--outfile=${outfile}`, - ], - { - cwd: packageRoot, - stdio: "inherit", - env: process.env, - }, - ); - - if (result.error) { - throw result.error; - } - - if (result.status !== 0) { - process.exit(result.status ?? 1); - } - - if (existsSync(outfile) && !outfile.endsWith(".exe")) { - chmodSync(outfile, 0o755); - } - - builtCount += 1; -} - -if (builtCount === 0) { - console.error( - `No binary targets selected from PROOFKIT_BINARY_TARGETS="${requestedTargetsEnv}". Valid targets: ${targets - .map((config) => config.target) - .join(", ")}`, - ); - process.exit(1); -} diff --git a/packages/cli/scripts/build-current-binary.mjs b/packages/cli/scripts/build-current-binary.mjs deleted file mode 100644 index 2a90d4f1..00000000 --- a/packages/cli/scripts/build-current-binary.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import { spawnSync } from "node:child_process"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const packageRoot = path.resolve(__dirname, ".."); -const target = `bun-${process.platform}-${process.arch}`; - -const result = spawnSync("node", ["./scripts/build-binaries.mjs"], { - cwd: packageRoot, - stdio: "inherit", - env: { - ...process.env, - PROOFKIT_BINARY_TARGETS: target, - }, -}); - -if (result.error) { - throw result.error; -} - -process.exit(result.status ?? 1); diff --git a/packages/cli/scripts/write-cli-version.mjs b/packages/cli/scripts/write-cli-version.mjs deleted file mode 100644 index 9e59ec03..00000000 --- a/packages/cli/scripts/write-cli-version.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import { readFileSync, writeFileSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const packageRoot = path.resolve(__dirname, ".."); - -const readVersion = (packagePath) => { - const packageJson = JSON.parse(readFileSync(packagePath, "utf8")); - return packageJson.version ?? "0.0.0-private"; -}; - -const outputPath = path.join(packageRoot, "src", "package-versions.ts"); -const content = [ - `export const CLI_VERSION: string = ${JSON.stringify(readVersion(path.join(packageRoot, "package.json")))};`, - `export const FMDAPI_VERSION = ${JSON.stringify(readVersion(path.join(packageRoot, "..", "fmdapi", "package.json")))} as const;`, - `export const BETTER_AUTH_VERSION = ${JSON.stringify(readVersion(path.join(packageRoot, "..", "better-auth", "package.json")))} as const;`, - `export const WEBVIEWER_VERSION = ${JSON.stringify(readVersion(path.join(packageRoot, "..", "webviewer", "package.json")))} as const;`, - `export const TYPEGEN_VERSION = ${JSON.stringify(readVersion(path.join(packageRoot, "..", "typegen", "package.json")))} as const;`, - "", -].join("\n"); - -writeFileSync(outputPath, content, "utf8"); diff --git a/packages/cli/src/cli/add/addon.ts b/packages/cli/src/cli/add/addon.ts deleted file mode 100644 index 0eff9038..00000000 --- a/packages/cli/src/cli/add/addon.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Command } from "commander"; -import { select } from "~/cli/prompts.js"; -import { debugOption, nonInteractiveOption } from "~/globalOptions.js"; -import { installFmAddonExplicitly } from "~/installers/install-fm-addon.js"; -import { initProgramState, isNonInteractiveMode } from "~/state.js"; -import { abortIfCancel } from "../utils.js"; - -type AddonTarget = "webviewer" | "auth"; - -async function resolveAddonTarget(name?: string): Promise { - if (name === "webviewer" || name === "auth") { - return name; - } - - if (isNonInteractiveMode()) { - throw new Error("Addon target is required in non-interactive mode. Use `proofkit add addon webviewer`."); - } - - return abortIfCancel( - await select({ - message: "Which add-on do you want to install locally?", - options: [ - { - value: "webviewer", - label: "Web Viewer", - hint: "ProofKit Web Viewer add-on", - }, - { value: "auth", label: "Auth", hint: "ProofKit Auth add-on" }, - ], - }), - ) as AddonTarget; -} - -export async function runAddAddonAction(targetName?: string) { - const target = await resolveAddonTarget(targetName); - - await installFmAddonExplicitly({ - addonName: target === "webviewer" ? "wv" : "auth", - }); -} - -export const makeAddAddonCommand = () => { - const addAddonCommand = new Command("addon") - .description("Install or update local FileMaker add-on files") - .argument("[target]", "Add-on to install locally (webviewer or auth)") - .addOption(nonInteractiveOption) - .addOption(debugOption) - .action(async (target) => { - await runAddAddonAction(target); - }); - - addAddonCommand.hook("preAction", (thisCommand) => { - initProgramState(thisCommand.opts()); - }); - - return addAddonCommand; -}; diff --git a/packages/cli/src/cli/add/auth.ts b/packages/cli/src/cli/add/auth.ts deleted file mode 100644 index a6c7e911..00000000 --- a/packages/cli/src/cli/add/auth.ts +++ /dev/null @@ -1,109 +0,0 @@ -import chalk from "chalk"; -import { Command } from "commander"; -import { z } from "zod/v4"; -import { cancel, select } from "~/cli/prompts.js"; - -import { addAuth } from "~/generators/auth.js"; -import { debugOption, nonInteractiveOption } from "~/globalOptions.js"; -import { initProgramState, isNonInteractiveMode, state } from "~/state.js"; -import { getSettings } from "~/utils/parseSettings.js"; -import { abortIfCancel } from "../utils.js"; - -export async function runAddAuthAction() { - const settings = getSettings(); - if (settings.appType !== "browser") { - return cancel("Auth is not supported for your app type."); - } - if (settings.ui === "shadcn") { - return cancel("Adding auth is not yet supported for shadcn-based projects."); - } - - const authType = - state.authType ?? - abortIfCancel( - await select({ - message: "What auth provider do you want to use?", - options: [ - { - value: "fmaddon", - label: "FM Add-on Auth", - hint: "Self-hosted auth with email/password", - }, - { - value: "clerk", - label: "Clerk", - hint: "Hosted auth service with many providers", - }, - ], - }), - ); - - const type = z.enum(["clerk", "fmaddon"]).parse(authType); - state.authType = type; - - if (type === "fmaddon") { - const emailProviderAnswer = - state.emailProvider ?? - (isNonInteractiveMode() ? "none" : undefined) ?? - abortIfCancel( - await select({ - message: `What email provider do you want to use?\n${chalk.dim( - "Used to send email verification codes. If you skip this, the codes will be displayed here in your terminal.", - )}`, - options: [ - { - label: "Resend", - value: "resend", - hint: "Great dev experience", - }, - { - label: "Plunk", - value: "plunk", - hint: "Cheapest for <20k emails/mo, self-hostable", - }, - { label: "Other / I'll do it myself later", value: "none" }, - ], - }), - ); - - const emailProvider = z.enum(["plunk", "resend", "none"]).parse(emailProviderAnswer); - - state.emailProvider = emailProvider; - - await addAuth({ - options: { - type, - emailProvider: emailProvider === "none" ? undefined : emailProvider, - }, - }); - } else { - await addAuth({ options: { type } }); - } -} - -export const makeAddAuthCommand = () => { - const addAuthCommand = new Command("auth") - .description("Add authentication to your project") - .option("--authType ", "Type of auth provider to use") - .option("--emailProvider ", "Email provider to use (only for FM Add-on Auth)") - .option("--apiKey ", "API key to use for the email provider (only for FM Add-on Auth)") - .addOption(nonInteractiveOption) - .addOption(debugOption) - - .action(async () => { - const settings = getSettings(); - if (settings.ui === "shadcn") { - throw new Error("`proofkit add auth` is no longer supported for shadcn projects"); - } - if (settings.auth.type !== "none") { - throw new Error("Auth already exists"); - } - await runAddAuthAction(); - }); - - addAuthCommand.hook("preAction", (thisCommand) => { - initProgramState(thisCommand.opts()); - }); - - return addAuthCommand; -}; diff --git a/packages/cli/src/cli/add/data-source/deploy-demo-file.ts b/packages/cli/src/cli/add/data-source/deploy-demo-file.ts deleted file mode 100644 index 7d460058..00000000 --- a/packages/cli/src/cli/add/data-source/deploy-demo-file.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { createDataAPIKeyWithCredentials, getDeploymentStatus, startDeployment } from "~/cli/ottofms.js"; -import * as p from "~/cli/prompts.js"; - -export const filename = "ProofKitDemo.fmp12"; - -export async function deployDemoFile({ - url, - token, - operation, -}: { - url: URL; - token: string; - operation: "install" | "replace"; -}): Promise<{ apiKey: string }> { - const deploymentJSON = { - scheduled: false, - label: "Install ProofKit Demo", - deployments: [ - { - name: "Install ProofKit Demo", - source: { - type: "url", - url: "https://proofkit.proof.sh/proofkit-demo/manifest.json", - }, - fileOperations: [ - { - target: { - fileName: filename, - }, - operation, - source: { - fileName: "ProofKitDemo.fmp12", - }, - location: { - folder: "default", - subFolder: "", - }, - }, - ], - concurrency: 1, - options: { - closeFilesAfterBuild: false, - keepFilesClosedAfterComplete: false, - transferContainerData: false, - }, - }, - ], - abortRemaining: false, - }; - - const spinner = p.spinner(); - spinner.start("Deploying ProofKit Demo file..."); - - const { - response: { subDeploymentIds }, - } = await startDeployment({ - payload: deploymentJSON, - url, - token, - }); - - const deploymentId = subDeploymentIds[0]; - if (!deploymentId) { - throw new Error("No deployment ID returned from the server"); - } - - while (true) { - // wait 2.5 seconds, then poll the status again - await new Promise((resolve) => setTimeout(resolve, 2500)); - - const { - response: { status, running }, - } = await getDeploymentStatus({ - url, - token, - deploymentId, - }); - if (!running) { - if (status !== "complete") { - throw new Error("Deployment didn't complete"); - } - break; - } - } - - const { apiKey } = await createDataAPIKeyWithCredentials({ - filename, - username: "admin", - password: "admin", - url, - }); - - spinner.stop(); - - return { apiKey }; -} diff --git a/packages/cli/src/cli/add/data-source/filemaker.ts b/packages/cli/src/cli/add/data-source/filemaker.ts deleted file mode 100644 index 3e2c4356..00000000 --- a/packages/cli/src/cli/add/data-source/filemaker.ts +++ /dev/null @@ -1,437 +0,0 @@ -import chalk from "chalk"; -import { SemVer } from "semver"; -import type { z } from "zod/v4"; -import { createDataAPIKey, getOttoFMSToken, listAPIKeys, listFiles } from "~/cli/ottofms.js"; -import * as p from "~/cli/prompts.js"; -import { abortIfCancel } from "~/cli/utils.js"; -import { addLayout, addToFmschemaConfig, ensureWebviewerFmMcpConfig } from "~/generators/fmdapi.js"; -import { getFmMcpStatus } from "~/helpers/fmMcp.js"; -import { fetchServerVersions } from "~/helpers/version-fetcher.js"; -import { isNonInteractiveMode } from "~/state.js"; -import { addPackageDependency } from "~/utils/addPackageDependency.js"; -import { addToEnv } from "~/utils/addToEnvs.js"; -import { type dataSourceSchema, getSettings, setSettings } from "~/utils/parseSettings.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; -import { validateAppName } from "~/utils/validateAppName.js"; -import { runAddSchemaAction } from "../fmschema.js"; -import { deployDemoFile, filename } from "./deploy-demo-file.js"; - -export async function promptForFileMakerDataSource({ - projectDir, - ...opts -}: { - projectDir: string; - name?: string; - server?: string; - adminApiKey?: string; - fileName?: string; - dataApiKey?: string; - layoutName?: string; - schemaName?: string; -}) { - const settings = getSettings(); - - if (settings.appType === "webviewer") { - const fmMcpStatus = await getFmMcpStatus(); - const connectedFileName = fmMcpStatus.connectedFiles[0]; - const localDataSourceName = opts.name ?? "filemaker"; - - if (!opts.server && fmMcpStatus.healthy && connectedFileName) { - addPackageDependency({ - projectDir, - dependencies: ["@proofkit/fmdapi"], - devMode: false, - }); - - await ensureWebviewerFmMcpConfig({ - projectDir, - connectedFileName, - dataSourceName: localDataSourceName, - baseUrl: fmMcpStatus.baseUrl, - }); - - // Persist the datasource in project settings - const newDataSource: z.infer = { - type: "fm", - name: localDataSourceName, - envNames: - localDataSourceName === "filemaker" - ? { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - } - : { - database: `${localDataSourceName.toUpperCase()}_FM_DATABASE`, - server: `${localDataSourceName.toUpperCase()}_FM_SERVER`, - apiKey: `${localDataSourceName.toUpperCase()}_OTTO_API_KEY`, - }, - }; - settings.dataSources.push(newDataSource); - setSettings(settings); - - if (opts.layoutName && opts.schemaName) { - await addLayout({ - projectDir, - dataSourceName: localDataSourceName, - schemas: [ - { - layoutName: opts.layoutName, - schemaName: opts.schemaName, - valueLists: "allowEmpty", - }, - ], - }); - } else if (opts.layoutName || opts.schemaName) { - throw new Error("Both --layoutName and --schemaName must be provided together."); - } else { - p.note( - `Detected local FM MCP at ${fmMcpStatus.baseUrl} with connected file "${connectedFileName}". Edit ${chalk.cyan( - "proofkit-typegen.config.jsonc", - )} to add layouts, then run ${chalk.cyan("pnpm typegen")} or ${chalk.cyan("pnpm typegen:ui")}.`, - "Local FileMaker detected", - ); - } - - return; - } - - if (!opts.server && isNonInteractiveMode()) { - throw new Error( - "No local FM MCP connection was detected and no FileMaker server was provided. Start the local FM MCP proxy with a connected file or rerun with --server.", - ); - } - - if (!opts.server) { - const fallbackAction = abortIfCancel( - await p.select({ - message: - "Local FM MCP was not detected. Do you want to continue with hosted FileMaker server setup or skip for now?", - options: [ - { - label: "Continue with hosted setup", - value: "hosted", - }, - { - label: "Skip for now", - value: "skip", - }, - ], - }), - ); - - if (fallbackAction === "skip") { - p.note( - `You can come back later with ${chalk.cyan("proofkit add data")} after starting FM MCP locally or when you have a hosted server ready.`, - ); - return; - } - } - } - - const existingFmDataSourceNames = settings.dataSources.filter((ds) => ds.type === "fm").map((ds) => ds.name); - - const server = await getValidFileMakerServerUrl(opts.server); - - const canDoBrowserLogin = server.ottoVersion && server.ottoVersion.compare(new SemVer("4.7.0")) > 0; - - if (!(canDoBrowserLogin || opts.adminApiKey)) { - return p.cancel( - "OttoFMS 4.7.0 or later is required to auto-login with this CLI. Please install/upgrade OttoFMS on your server, or pass an Admin API key with the --adminApiKey flag then try again", - ); - } - - const token = opts.adminApiKey || (await getOttoFMSToken({ url: server.url })).token; - - const fileList = await listFiles({ url: server.url, token }); - const demoFileExists = fileList.map((f) => f.filename.replace(".fmp12", "")).includes(filename.replace(".fmp12", "")); - let fmFile = opts.fileName; - while (true) { - fmFile = - opts.fileName || - abortIfCancel( - await p.searchSelect({ - message: `Which file would you like to connect to? ${chalk.dim("(TIP: Select the file where your data is stored)")}`, - emptyMessage: "No matching files found.", - options: [ - { - value: "$deployDemoFile", - label: "Deploy NEW ProofKit Demo File", - hint: "Use OttoFMS to deploy a new file for testing", - keywords: ["demo", "proofkit"], - }, - ...fileList - .sort((a, b) => a.filename.localeCompare(b.filename)) - .map((file) => ({ - value: file.filename, - label: file.filename, - hint: file.status, - keywords: [file.filename], - })), - ], - }), - ); - - if (fmFile !== "$deployDemoFile") { - break; - } - - if (demoFileExists) { - const replace = abortIfCancel( - await p.confirm({ - message: "The demo file already exists, do you want to replace it with a fresh copy?", - initialValue: false, - }), - ); - if (replace) { - break; - } - } else { - break; - } - } - - if (!fmFile) { - throw new Error("No file selected"); - } - - let dataApiKey = opts.dataApiKey; - if (fmFile === "$deployDemoFile") { - const { apiKey } = await deployDemoFile({ - url: server.url, - token, - operation: demoFileExists ? "replace" : "install", - }); - dataApiKey = apiKey; - fmFile = filename; - opts.layoutName = opts.layoutName ?? "API_Contacts"; - opts.schemaName = opts.schemaName ?? "Contacts"; - } else { - const allApiKeys = await listAPIKeys({ url: server.url, token }); - const thisFileApiKeys = allApiKeys.filter((key) => key.database === fmFile); - - if (!dataApiKey && thisFileApiKeys.length > 0) { - const selectedKey = abortIfCancel( - await p.searchSelect({ - message: `Which OttoFMS Data API key would you like to use? ${chalk.dim(`(This determines the access that you'll have to the data in this file)`)}`, - emptyMessage: "No matching API keys found.", - options: [ - ...thisFileApiKeys.map((key) => ({ - value: key.key, - label: `${chalk.bold(key.label)} - ${key.user}`, - hint: `${key.key.slice(0, 5)}...${key.key.slice(-4)}`, - keywords: [key.label, key.user, key.database], - })), - { - value: "create", - label: "Create a new API key", - hint: "Requires FileMaker credentials for this file", - keywords: ["create", "new"], - }, - ], - }), - ); - if (typeof selectedKey !== "string") { - throw new Error("Invalid key"); - } - if (selectedKey !== "create") { - dataApiKey = selectedKey; - } - } - - if (!dataApiKey) { - // data api was not provided, prompt to create a new one - const resp = await createDataAPIKey({ - filename: fmFile, - url: server.url, - }); - dataApiKey = resp.apiKey; - } - } - if (!dataApiKey) { - throw new Error("No API key"); - } - - const name = - existingFmDataSourceNames.length === 0 - ? "filemaker" - : (opts.name ?? - abortIfCancel( - await p.text({ - message: "What do you want to call this data source?", - validate: (value) => { - if (value === "filemaker") { - return "That name is reserved"; - } - - // require name to be unique - if (existingFmDataSourceNames?.includes(value)) { - return "That name is already in use in this project, pick something unique"; - } - - // require name to be alphanumeric, lowercase, etc - return validateAppName(value); - }, - }), - )); - - const newDataSource: z.infer = { - type: "fm", - name, - envNames: - name === "filemaker" - ? { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - } - : { - database: `${name.toUpperCase()}_FM_DATABASE`, - server: `${name.toUpperCase()}_FM_SERVER`, - apiKey: `${name.toUpperCase()}_OTTO_API_KEY`, - }, - }; - - const project = getNewProject(projectDir); - - const schemaFile = await addToEnv({ - projectDir, - project, - envs: [ - { - name: newDataSource.envNames.database, - zodValue: `z.string().endsWith(".fmp12")`, - defaultValue: fmFile, - type: "server", - }, - { - name: newDataSource.envNames.server, - zodValue: "z.string().url()", - type: "server", - defaultValue: server.url.origin, - }, - { - name: newDataSource.envNames.apiKey, - zodValue: `z.string().startsWith("dk_") as z.ZodType`, - type: "server", - defaultValue: dataApiKey, - }, - ], - }); - - const fmdapiImport = schemaFile.getImportDeclaration((imp) => imp.getModuleSpecifierValue() === "@proofkit/fmdapi"); - if (fmdapiImport) { - fmdapiImport - .getNamedImports() - .find((imp) => imp.getName() === "OttoAPIKey") - ?.remove(); - fmdapiImport.addNamedImport({ name: "OttoAPIKey", isTypeOnly: true }); - } else { - schemaFile.addImportDeclaration({ - namedImports: [{ name: "OttoAPIKey", isTypeOnly: true }], - moduleSpecifier: "@proofkit/fmdapi", - }); - } - - addPackageDependency({ - projectDir, - dependencies: ["@proofkit/fmdapi"], - devMode: false, - }); - - settings.dataSources.push(newDataSource); - setSettings(settings); - - addToFmschemaConfig({ - dataSourceName: name, - envNames: name === "filemaker" ? undefined : newDataSource.envNames, - }); - - await formatAndSaveSourceFiles(project); - - // now prompt for layout - await runAddSchemaAction({ - settings, - sourceName: name, - projectDir, - layoutName: opts.layoutName, - schemaName: opts.schemaName, - valueLists: "allowEmpty", - }); -} - -async function getValidFileMakerServerUrl(defaultServerUrl?: string | undefined): Promise<{ - url: URL; - fmsVersion: SemVer; - ottoVersion: SemVer | null; -}> { - const spinner = p.spinner(); - let url: URL | null = null; - let fmsVersion: SemVer | null = null; - let ottoVersion: SemVer | null = null; - let serverUrlToUse = defaultServerUrl; - - while (fmsVersion === null) { - const serverUrl = - serverUrlToUse ?? - abortIfCancel( - await p.text({ - message: `What is the URL of your FileMaker Server?\n${chalk.cyan("TIP: You can copy any valid path on the server and paste it here.")}`, - validate: (value) => { - try { - // try to make sure the url is https - let normalizedValue = value; - if (!normalizedValue.startsWith("https://")) { - if (normalizedValue.startsWith("http://")) { - normalizedValue = normalizedValue.replace("http://", "https://"); - } else { - normalizedValue = `https://${normalizedValue}`; - } - } - - // try to make sure the url is valid - new URL(normalizedValue); - return; - } catch { - return "Please enter a valid URL"; - } - }, - }), - ); - - try { - url = new URL(serverUrl); - } catch { - p.log.error(`Invalid URL: ${serverUrl.toString()}`); - continue; - } - - spinner.start("Validating Server URL..."); - - // check for FileMaker and Otto versions - const { fmsInfo, ottoInfo } = await fetchServerVersions({ - url: url.origin, - }); - - spinner.stop(); - - const fmsVersionString = fmsInfo.ServerVersion.split(" ")[0]; - if (!fmsVersionString) { - p.log.error("Unable to parse FileMaker Server version"); - serverUrlToUse = undefined; - continue; - } - fmsVersion = new SemVer(fmsVersionString); - ottoVersion = ottoInfo?.Otto.version ? new SemVer(ottoInfo.Otto.version) : null; - serverUrlToUse = undefined; - } - - if (url === null) { - throw new Error("Unable to get FileMaker Server URL"); - } - - p.note(`๐ŸŽ‰ FileMaker Server version ${fmsVersion} detected \n - ${ottoVersion ? `๐ŸŽ‰ OttoFMS version ${ottoVersion} detected` : "โŒ OttoFMS not detected"}`); - - return { url, ottoVersion, fmsVersion }; -} diff --git a/packages/cli/src/cli/add/data-source/index.ts b/packages/cli/src/cli/add/data-source/index.ts deleted file mode 100644 index 647db728..00000000 --- a/packages/cli/src/cli/add/data-source/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Command } from "commander"; -import { z } from "zod/v4"; -import * as p from "~/cli/prompts.js"; - -import { ensureProofKitProject } from "~/cli/utils.js"; -import { nonInteractiveOption } from "~/globalOptions.js"; -import { initProgramState } from "~/state.js"; -import { promptForFileMakerDataSource } from "./filemaker.js"; - -const dataSourceType = z.enum(["fm", "supabase"]); -export const runAddDataSourceCommand = async () => { - const dataSource = dataSourceType.parse( - await p.select({ - message: "Which data souce do you want to add?", - options: [ - { label: "FileMaker", value: "fm" }, - { label: "Supabase", value: "supabase" }, - ], - }), - ); - - if (dataSource === "supabase") { - throw new Error("Not implemented"); - } - if (dataSource === "fm") { - await promptForFileMakerDataSource({ projectDir: process.cwd() }); - } else { - throw new Error("Invalid data source"); - } -}; - -export const makeAddDataSourceCommand = () => { - const addDataSourceCommand = new Command("data"); - addDataSourceCommand.description("Add a new data source to your project"); - addDataSourceCommand.addOption(nonInteractiveOption); - - addDataSourceCommand.hook("preAction", (_thisCommand, actionCommand) => { - initProgramState(actionCommand.opts()); - const settings = ensureProofKitProject({ commandName: "add" }); - actionCommand.setOptionValue("settings", settings); - }); - - // addDataSourceCommand.action(); - return addDataSourceCommand; -}; diff --git a/packages/cli/src/cli/add/fmschema.ts b/packages/cli/src/cli/add/fmschema.ts deleted file mode 100644 index 35430779..00000000 --- a/packages/cli/src/cli/add/fmschema.ts +++ /dev/null @@ -1,214 +0,0 @@ -import path from "node:path"; -import type { OttoAPIKey } from "@proofkit/fmdapi"; -import type { ValueListsOptions } from "@proofkit/typegen/config"; -import chalk from "chalk"; -import { Command } from "commander"; -import dotenv from "dotenv"; -import { z } from "zod/v4"; -import * as p from "~/cli/prompts.js"; -import { addLayout, getExistingSchemas } from "~/generators/fmdapi.js"; -import { state } from "~/state.js"; -import { getSettings, type Settings } from "~/utils/parseSettings.js"; -import { commonFileMakerLayoutPrefixes, getLayouts } from "../fmdapi.js"; -import { abortIfCancel } from "../utils.js"; - -// Regex to validate JavaScript variable names -const VALID_JS_VARIABLE_NAME = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; - -export const runAddSchemaAction = async (opts?: { - projectDir?: string; - settings: Settings; - sourceName?: string; - layoutName?: string; - schemaName?: string; - valueLists?: ValueListsOptions; -}) => { - const settings = getSettings(); - const projectDir = state.projectDir; - let sourceName = opts?.sourceName; - if (sourceName) { - sourceName = opts?.sourceName; - } else if (settings.dataSources.filter((s) => s.type === "fm").length > 1) { - // if there is more than one fm data source, we need to prompt for which one to add the layout to - const dataSourceName = await p.select({ - message: "Which FileMaker data source do you want to add a layout to?", - options: settings.dataSources.filter((s) => s.type === "fm").map((s) => ({ label: s.name, value: s.name })), - }); - if (p.isCancel(dataSourceName)) { - p.cancel(); - process.exit(0); - } - sourceName = z.string().parse(dataSourceName); - } - - if (!sourceName) { - sourceName = "filemaker"; - } - - const dataSource = settings.dataSources.filter((s) => s.type === "fm").find((s) => s.name === sourceName); - if (!dataSource) { - throw new Error(`FileMaker data source ${sourceName} not found in your ProofKit config`); - } - - const spinner = p.spinner(); - spinner.start("Loading layouts from your FileMaker file..."); - if (settings.envFile) { - dotenv.config({ - path: path.join(projectDir, settings.envFile), - }); - } - - const dataApiKey = process.env[dataSource.envNames.apiKey]; - const fmFile = process.env[dataSource.envNames.database]; - const server = process.env[dataSource.envNames.server]; - - if (!(dataApiKey && fmFile && server)) { - spinner.stop("Failed to load layouts"); - p.cancel("Missing required environment variables. Please check your .env file."); - process.exit(1); - } - - // Validate API key format - if (!(dataApiKey.startsWith("KEY_") || dataApiKey.startsWith("dk_"))) { - spinner.stop("Failed to load layouts"); - p.cancel("Invalid API key format. API key must start with 'KEY_' or 'dk_'."); - process.exit(1); - } - - // Type assertion after validation - const validatedApiKey: OttoAPIKey = dataApiKey as OttoAPIKey; - - const layouts = await getLayouts({ - dataApiKey: validatedApiKey, - fmFile, - server, - }); - - const existingConfigResults = getExistingSchemas({ - projectDir, - dataSourceName: sourceName, - }); - - const existingLayouts = existingConfigResults.map((s) => s.layout).filter(Boolean); - - const existingSchemas = existingConfigResults.map((s) => s.schemaName).filter(Boolean); - - spinner.stop("Loaded layouts from your FileMaker file"); - - if (existingLayouts.length > 0) { - p.note(existingLayouts.join("\n"), "Detected existing layouts in your project"); - } - - // list other common layout names to exclude - existingLayouts.push("-"); - - let passedInLayoutName: string | undefined = opts?.layoutName; - if (passedInLayoutName === "" || !layouts.includes(passedInLayoutName ?? "")) { - passedInLayoutName = undefined; - } - - const selectedLayout = - passedInLayoutName ?? - abortIfCancel( - await p.searchSelect({ - message: "Select a new layout to read data from", - emptyMessage: "No matching layouts found.", - options: layouts - .filter((layout) => !existingLayouts.includes(layout)) - .map((layout) => ({ - label: layout, - value: layout, - keywords: [layout], - })), - }), - ); - - const defaultSchemaName = getDefaultSchemaName(selectedLayout); - const schemaName = - opts?.schemaName || - abortIfCancel( - await p.text({ - message: `Enter a friendly name for the new schema.\n${chalk.dim("This will the name by which you refer to this layout in your codebase")}`, - // initialValue: selectedLayout, - defaultValue: defaultSchemaName, - validate: (input) => { - if (input === "") { - return; // allow empty input for the default value - } - // ensure the input is a valid JS variable name - if (!VALID_JS_VARIABLE_NAME.test(input)) { - return "Name must consist of only alphanumeric characters, '_', and must not start with a number"; - } - if (existingSchemas.includes(input)) { - return "Schema name must be unique"; - } - return; - }, - }), - ).toString(); - - const valueLists = - opts?.valueLists ?? - ((await p.select({ - message: `Should we use value lists on this layout?\n${chalk.dim( - "This will allow fields that contain a value list to be auto-completed in typescript and also validated to prevent incorrect values", - )}`, - options: [ - { - label: "Yes, but allow empty fields", - value: "allowEmpty", - hint: "Empty fields or values that don't match the value list will be converted to an empty string", - }, - { - label: "Yes; empty values should fail validation", - value: "strict", - hint: "Empty fields or values that don't match the value list will cause validation to fail", - }, - { - label: "No, ignore value lists", - value: "ignore", - hint: "Fields will just be typed as strings", - }, - ], - })) as ValueListsOptions); - - const valueListsValidated = z.enum(["ignore", "allowEmpty", "strict"]).catch("ignore").parse(valueLists); - - await addLayout({ - runCodegen: true, - projectDir, - dataSourceName: sourceName, - schemas: [ - { - layoutName: selectedLayout, - schemaName, - valueLists: valueListsValidated, - }, - ], - }); - - p.outro(`Layout "${selectedLayout}" added to your project as "${schemaName}"`); -}; - -export const makeAddSchemaCommand = () => { - const addSchemaCommand = new Command("layout") - .alias("schema") - .description("Add a new layout to your fmschema file") - .action(async (opts: { settings: Settings }) => { - const settings = opts.settings; - - await runAddSchemaAction({ settings }); - }); - - return addSchemaCommand; -}; - -function getDefaultSchemaName(layout: string) { - let schemaName = layout.replace(/[-\s]/g, "_"); - for (const prefix of commonFileMakerLayoutPrefixes) { - if (schemaName.startsWith(prefix)) { - schemaName = schemaName.replace(prefix, ""); - } - } - return schemaName; -} diff --git a/packages/cli/src/cli/add/index.ts b/packages/cli/src/cli/add/index.ts deleted file mode 100644 index 19f429e6..00000000 --- a/packages/cli/src/cli/add/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { logger } from "~/utils/logger.js"; -import { runAddAddonAction } from "./addon.js"; - -const ADDON_ONLY_MESSAGE = "Only `proofkit add addon ` is supported."; - -export const runAdd = async (name: string | undefined, options?: { noInstall?: boolean; target?: string }) => { - if (name === "addon") { - return await runAddAddonAction(options?.target); - } - logger.error(ADDON_ONLY_MESSAGE); - throw new Error(ADDON_ONLY_MESSAGE); -}; diff --git a/packages/cli/src/cli/add/page/index.ts b/packages/cli/src/cli/add/page/index.ts deleted file mode 100644 index 6b664273..00000000 --- a/packages/cli/src/cli/add/page/index.ts +++ /dev/null @@ -1,231 +0,0 @@ -import path from "node:path"; -import chalk from "chalk"; -import { Command } from "commander"; -import { capitalize } from "es-toolkit"; -import fs from "fs-extra"; -import { nextjsTemplates, wvTemplates } from "~/cli/add/page/templates.js"; -import * as p from "~/cli/prompts.js"; -import { PKG_ROOT } from "~/consts.js"; -import { getExistingSchemas } from "~/generators/fmdapi.js"; -import { addRouteToNav } from "~/generators/route.js"; -import { debugOption, nonInteractiveOption } from "~/globalOptions.js"; -import { initProgramState, isNonInteractiveMode, state } from "~/state.js"; -import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; -import { type DataSource, getSettings, mergeSettings } from "~/utils/parseSettings.js"; -import { abortIfCancel, ensureProofKitProject } from "../../utils.js"; - -export const runAddPageAction = async (opts?: { - routeName?: string; - pageName?: string; - dataSourceName?: string; - schemaName?: string; - template?: string; -}) => { - const projectDir = state.projectDir; - - const settings = getSettings(); - if (settings.ui === "shadcn") { - return p.cancel("Adding pages is not yet supported for shadcn-based projects."); - } - - const templates = state.appType === "browser" ? Object.entries(nextjsTemplates) : Object.entries(wvTemplates); - - if (templates.length === 0) { - return p.cancel("No templates found for your app type. Check back soon!"); - } - - let routeName = opts?.routeName; - let replacedMainPage = settings.replacedMainPage; - - if (state.appType === "webviewer" && !replacedMainPage && !isNonInteractiveMode() && !routeName) { - const replaceMainPage = abortIfCancel( - await p.select({ - message: "Do you want to replace the default page?", - options: [ - { label: "Yes", value: "yes" }, - { label: "No, maybe later", value: "no" }, - { label: "No, don't ask again", value: "never" }, - ], - }), - ); - if (replaceMainPage === "never" || replaceMainPage === "yes") { - replacedMainPage = true; - } - - if (replaceMainPage === "yes") { - routeName = "/"; - } - } - - if (!routeName) { - routeName = abortIfCancel( - await p.text({ - message: "Enter the URL PATH for your new page", - validate: (value) => { - if (value.length === 0) { - return "URL path is required"; - } - return; - }, - }), - ); - } - - if (!routeName.startsWith("/")) { - routeName = `/${routeName}`; - } - - const pageName = capitalize(routeName.replace("/", "").trim()); - - const template = - opts?.template ?? - abortIfCancel( - await p.select({ - message: "What template should be used for this page?", - options: templates.map(([key, value]) => ({ - value: key, - label: `${value.label}`, - hint: value.hint, - })), - }), - ); - - const pageTemplate = templates.find(([key]) => key === template)?.[1]; - if (!pageTemplate) { - return p.cancel(`Page template ${template} not found`); - } - - let dataSource: DataSource | undefined; - let schemaName: string | undefined; - if (pageTemplate.requireData) { - if (settings.dataSources.length === 0) { - return p.cancel( - "This template requires a data source, but you don't have any. Add a data source first, or choose another page template", - ); - } - - const dataSourceName = - opts?.dataSourceName ?? - (settings.dataSources.length > 1 - ? abortIfCancel( - await p.select({ - message: "Which data source should be used for this page?", - options: settings.dataSources.map((dataSource) => ({ - value: dataSource.name, - label: dataSource.name, - })), - }), - ) - : settings.dataSources[0]?.name); - - dataSource = settings.dataSources.find((dataSource) => dataSource.name === dataSourceName); - if (!dataSource) { - return p.cancel(`Data source ${dataSourceName} not found`); - } - - schemaName = await promptForSchemaFromDataSource({ - projectDir, - dataSource, - }); - } - - const spinner = p.spinner(); - spinner.start("Adding page from template"); - - // copy template files - const templatePath = path.join(PKG_ROOT, "template/pages", pageTemplate.templatePath); - - const destPath = - state.appType === "browser" - ? path.join(projectDir, "src/app/(main)", routeName) - : path.join(projectDir, "src/routes", routeName); - - await fs.copy(templatePath, destPath); - - if (state.appType === "browser") { - if (pageName && pageName !== "") { - await addRouteToNav({ - projectDir: process.cwd(), - navType: "primary", - label: pageName, - href: routeName, - }); - } - } else if (state.appType === "webviewer") { - // TODO: implement - } - // call post-install function - await pageTemplate.postIntallFn?.({ - projectDir, - pageDir: destPath, - dataSource, - schemaName, - }); - - if (replacedMainPage !== settings.replacedMainPage) { - // avoid changing this until the end since the user could cancel early - mergeSettings({ replacedMainPage }); - } - - spinner.stop("Added page!"); - const pkgManager = getUserPkgManager(); - - console.log( - `\n${chalk.green("Next steps:")}\nTo preview this page, restart your dev server using the ${chalk.cyan(`${pkgManager === "npm" ? "npm run" : pkgManager} dev`)} command\n`, - ); -}; - -export const makeAddPageCommand = () => { - const addPageCommand = new Command("page").description("Add a new page to your project").action(async () => { - await runAddPageAction(); - }); - - addPageCommand.addOption(nonInteractiveOption); - addPageCommand.addOption(debugOption); - - addPageCommand.hook("preAction", () => { - initProgramState(addPageCommand.opts()); - state.baseCommand = "add"; - ensureProofKitProject({ commandName: "add" }); - }); - - return addPageCommand; -}; - -async function promptForSchemaFromDataSource({ - projectDir = process.cwd(), - dataSource, -}: { - projectDir?: string; - dataSource: DataSource; -}) { - if (dataSource.type === "supabase") { - throw new Error("Not implemented"); - } - const schemas = getExistingSchemas({ - projectDir, - dataSourceName: dataSource.name, - }) - .map((s) => s.schemaName) - .filter(Boolean); - - if (schemas.length === 0) { - p.cancel("This data source doesn't have any schemas to load data from"); - return undefined; - } - - if (schemas.length === 1) { - return schemas[0]; - } - - const schemaName = abortIfCancel( - await p.select({ - message: "Which schema should this page load data from?", - options: schemas.map((schema) => ({ - label: schema ?? "", - value: schema ?? "", - })), - }), - ); - return schemaName; -} diff --git a/packages/cli/src/cli/add/page/post-install/table-infinite.ts b/packages/cli/src/cli/add/page/post-install/table-infinite.ts deleted file mode 100644 index fcccbb27..00000000 --- a/packages/cli/src/cli/add/page/post-install/table-infinite.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { injectTanstackQuery } from "~/generators/tanstack-query.js"; -import { installDependencies } from "~/helpers/installDependencies.js"; -import type { TPostInstallFn } from "../types.js"; -import { postInstallTable } from "./table.js"; - -export const postInstallTableInfinite: TPostInstallFn = async (args) => { - await postInstallTable(args); - const didInject = await injectTanstackQuery(); - if (didInject) { - await installDependencies(); - } -}; diff --git a/packages/cli/src/cli/add/page/post-install/table.ts b/packages/cli/src/cli/add/page/post-install/table.ts deleted file mode 100644 index a6e930ed..00000000 --- a/packages/cli/src/cli/add/page/post-install/table.ts +++ /dev/null @@ -1,123 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; -import { SyntaxKind } from "ts-morph"; - -import { getClientSuffix, getFieldNamesForSchema } from "~/generators/fmdapi.js"; -import { injectTanstackQuery } from "~/generators/tanstack-query.js"; -import { installDependencies } from "~/helpers/installDependencies.js"; -import { state } from "~/state.js"; -import { getSettings } from "~/utils/parseSettings.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; -import type { TPostInstallFn } from "../types.js"; - -// Regex to validate JavaScript identifiers -const VALID_JS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; - -export const postInstallTable: TPostInstallFn = async ({ projectDir, pageDir, dataSource, schemaName }) => { - if (!dataSource) { - throw new Error("DataSource is required for table page"); - } - if (!schemaName) { - throw new Error("SchemaName is required for table page"); - } - if (dataSource.type !== "fm") { - throw new Error("FileMaker DataSource is required for table page"); - } - - const clientSuffix = getClientSuffix({ - projectDir, - dataSourceName: dataSource.name, - }); - - const allFieldNames = getFieldNamesForSchema({ - schemaName, - dataSourceName: dataSource.name, - }); - - const settings = getSettings(); - if (settings.ui === "shadcn") { - return; - } - const auth = settings.auth; - - const substitutions = { - __SOURCE_NAME__: dataSource.name, - __TYPE_NAME__: `T${schemaName}`, - __ZOD_TYPE_NAME__: `Z${schemaName}`, - __CLIENT_NAME__: `${schemaName}${clientSuffix}`, - __SCHEMA_NAME__: schemaName, - __ACTION_CLIENT__: auth.type === "none" ? "actionClient" : "authedActionClient", - __FIRST_FIELD_NAME__: allFieldNames[0] ?? "NO_FIELDS_ON_YOUR_LAYOUT", - }; - - // read all files in pageDir and loop over them - const files = await fs.readdir(pageDir); - for await (const file of files) { - const filePath = path.join(pageDir, file); - let fileContent = await fs.readFile(filePath, "utf8"); - - for (const [key, value] of Object.entries(substitutions)) { - fileContent = fileContent.replace(new RegExp(key, "g"), value); - } - - await fs.writeFile(filePath, fileContent, "utf8"); - } - - // add the schemas to the columns array - const project = getNewProject(projectDir); - const sourceFile = project.addSourceFileAtPath( - path.join(pageDir, state.appType === "browser" ? "table.tsx" : "index.tsx"), - ); - const columns = sourceFile.getVariableDeclaration("columns")?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression); - - const fieldNames = filterOutCommonFieldNames(allFieldNames.filter(Boolean) as string[]); - - for await (const fieldName of fieldNames) { - columns?.addElement((writer) => - writer - .inlineBlock(() => { - if (needsBracketNotation(fieldName)) { - writer.write(`accessorFn: (row) => row["${fieldName}"],`); - } else { - writer.write(`accessorFn: (row) => row.${fieldName},`); - } - writer.write(`header: "${fieldName}",`); - }) - .write(",") - .newLine(), - ); - } - - if (state.appType === "webviewer") { - const didInject = await injectTanstackQuery({ project }); - if (didInject) { - await installDependencies(); - } - } - - await formatAndSaveSourceFiles(project); -}; - -// Function to check if a field name needs bracket notation -function needsBracketNotation(fieldName: string): boolean { - // Check if it's a valid JavaScript identifier - return !VALID_JS_IDENTIFIER.test(fieldName); -} - -const commonFieldNamesToExclude = [ - "id", - "pk", - "createdat", - "updatedat", - "primarykey", - "createdby", - "modifiedby", - "creationtimestamp", - "modificationtimestamp", -]; - -function filterOutCommonFieldNames(fieldNames: string[]): string[] { - return fieldNames.filter( - (fieldName) => !commonFieldNamesToExclude.includes(fieldName.toLowerCase()) || fieldName.startsWith("_"), - ); -} diff --git a/packages/cli/src/cli/add/page/templates.ts b/packages/cli/src/cli/add/page/templates.ts deleted file mode 100644 index a49d5740..00000000 --- a/packages/cli/src/cli/add/page/templates.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { postInstallTable } from "./post-install/table.js"; -import { postInstallTableInfinite } from "./post-install/table-infinite.js"; -import type { TPostInstallFn } from "./types.js"; - -export interface Template { - requireData: boolean; - label: string; - hint?: string; - templatePath: string; - screenshot?: string; - tags?: string[]; - postIntallFn?: TPostInstallFn; -} - -export const nextjsTemplates: Record = { - blank: { - requireData: false, - label: "Blank", - templatePath: "nextjs/blank", - }, - table: { - requireData: true, - label: "Basic Table", - hint: "Use to load and show multiple records", - templatePath: "nextjs/table", - postIntallFn: postInstallTable, - }, - tableEdit: { - requireData: true, - label: "Basic Table (editable)", - hint: "Use to load and show multiple records with inline edit functionality", - templatePath: "nextjs/table-edit", - postIntallFn: postInstallTable, - }, - tableInfinite: { - requireData: true, - label: "Infinite Table", - hint: "Automatically load more records when the user scrolls to the bottom", - templatePath: "nextjs/table-infinite", - postIntallFn: postInstallTableInfinite, - }, - tableInfiniteEdit: { - requireData: true, - label: "Infinite Table (editable)", - hint: "Automatically load more records when the user scrolls to the bottom with inline edit functionality", - templatePath: "nextjs/table-infinite-edit", - postIntallFn: postInstallTableInfinite, - }, -}; - -export const wvTemplates: Record = { - blank: { - requireData: false, - label: "Blank", - templatePath: "vite-wv/blank", - }, - table: { - requireData: true, - label: "Basic Table", - hint: "Use to load and show multiple records", - templatePath: "vite-wv/table", - postIntallFn: postInstallTable, - }, - tableEdit: { - requireData: true, - label: "Basic Table (editable)", - hint: "Use to load and show multiple records with inline edit functionality", - templatePath: "vite-wv/table-edit", - postIntallFn: postInstallTable, - }, - // tableInfinite: { - // requireData: true, - // label: "Infinite Table", - // hint: "Automatically load more records when the user scrolls to the bottom", - // templatePath: "vite-wv/table-infinite", - // postIntallFn: postInstallTableInfinite, - // }, - // tableInfiniteEdit: { - // requireData: true, - // label: "Infinite Table (editable)", - // hint: "Automatically load more records when the user scrolls to the bottom with inline edit functionality", - // templatePath: "vite-wv/table-infinite-edit", - // postIntallFn: postInstallTableInfinite, - // }, -}; diff --git a/packages/cli/src/cli/add/page/types.ts b/packages/cli/src/cli/add/page/types.ts deleted file mode 100644 index 7b7da162..00000000 --- a/packages/cli/src/cli/add/page/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { DataSource } from "~/utils/parseSettings.js"; - -export type TPostInstallFn = (args: { - projectDir: string; - /** Path in the project where the pages were copyied to. */ - pageDir: string; - dataSource?: DataSource; - schemaName?: string; -}) => void | Promise; - -export interface Template { - requireData: boolean; - label: string; - hint?: string; - /** Path from the template/pages directory to the template files to copy. */ - templatePath: string; - /** Will be run after the page contents is created and copied into the project. */ - postIntallFn?: TPostInstallFn; -} diff --git a/packages/cli/src/cli/deploy/index.ts b/packages/cli/src/cli/deploy/index.ts deleted file mode 100644 index 6ec2b94d..00000000 --- a/packages/cli/src/cli/deploy/index.ts +++ /dev/null @@ -1,489 +0,0 @@ -import path from "node:path"; -import chalk from "chalk"; -import { Command, Option } from "commander"; -import { execa } from "execa"; -import fs from "fs-extra"; -import type { PackageJson } from "type-fest"; -import * as p from "~/cli/prompts.js"; - -import { debugOption, nonInteractiveOption } from "~/globalOptions.js"; - -// Regex patterns defined at top level for performance -const LEADING_SYMBOLS_REGEX = /^[โœ”\s]+/; -const MULTI_SPACE_REGEX = /\s{2,}/; -const VERSION_PREFIX_REGEX = /^v/; - -import { initProgramState, state } from "~/state.js"; -import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; -import { getSettings } from "~/utils/parseSettings.js"; -import { ensureProofKitProject } from "../utils.js"; - -async function checkVercelCLI(): Promise { - try { - await execa("vercel", ["--version"]); - return true; - } catch (_error) { - return false; - } -} - -async function installVercelCLI() { - const pkgManager = getUserPkgManager(); - const spinner = p.spinner(); - spinner.start("Installing Vercel CLI..."); - - try { - const installCmd = pkgManager === "npm" ? "install" : "add"; - await execa(pkgManager, [installCmd, "-g", "vercel"]); - spinner.stop("Vercel CLI installed successfully"); - return true; - } catch (error) { - spinner.stop("Failed to install Vercel CLI"); - console.error(chalk.red("Error installing Vercel CLI:"), error); - return false; - } -} - -async function checkVercelProject(): Promise { - try { - // Try to read the .vercel/project.json file which exists when a project is linked - const projectConfig = (await fs.readJSON(".vercel/project.json")) as VercelProjectConfig; - return Boolean(projectConfig.projectId); - } catch (_error) { - if (state.debug) { - console.log("\nDebug: No Vercel project configuration found"); - } - return false; - } -} - -async function getVercelTeams(): Promise<{ slug: string; name: string }[]> { - try { - if (state.debug) { - console.log("\nDebug: Running vercel teams list command..."); - } - - const result = await execa("vercel", ["teams", "list"], { - all: true, - }); - - if (state.debug) { - console.log("\nDebug: Command output:", result.all); - } - - const lines = (result.all ?? "").split("\n").filter(Boolean); - - // Find the index of the header line - const headerIndex = lines.findIndex((line) => line.includes("id")); - if (headerIndex === -1) { - return []; - } - - // Get only the lines after the header - const teamLines = lines.slice(headerIndex + 1); - - if (state.debug) { - console.log("\nDebug: Team lines:"); - for (const line of teamLines) { - console.log(`"${line}"`); - } - } - - const teams = teamLines - .map((line) => { - // Remove any leading symbols (โœ” or spaces) and trim - const cleanLine = line.replace(LEADING_SYMBOLS_REGEX, "").trim(); - // Split on multiple spaces and take the first part as slug, rest as name - const [slug, ...nameParts] = cleanLine.split(MULTI_SPACE_REGEX); - if (!slug || nameParts.length === 0) { - return null; - } - - return { - slug, - name: nameParts.join(" ").trim(), - }; - }) - .filter((team): team is { slug: string; name: string } => team !== null); - - if (state.debug) { - console.log("\nDebug: Parsed teams:", teams); - } - - return teams; - } catch (error) { - if (state.debug) { - console.error("Error getting Vercel teams:", error); - } - return []; - } -} - -async function setupVercelProject() { - const spinner = p.spinner(); - - try { - // Get project name from package.json - const pkgJson = (await fs.readJSON("package.json")) as PackageJson; - const projectName = pkgJson.name; - - // Get available teams - const teams = await getVercelTeams(); - - let teamFlag = ""; - if (teams.length > 1) { - const teamChoice = await p.select({ - message: "Select a team to deploy under:", - options: [ - ...teams.map((team) => ({ - value: team.slug, - label: team.name, - })), - ], - }); - - if (p.isCancel(teamChoice)) { - console.log(chalk.yellow("\nOperation cancelled")); - return false; - } - - if (teamChoice && typeof teamChoice === "string") { - teamFlag = `--scope=${teamChoice}`; - } - } - - spinner.start("Creating Vercel project..."); - - // Create project with default settings - await execa("vercel", ["link", "--yes", ...(teamFlag ? [teamFlag] : [])], { - env: { - VERCEL_PROJECT_NAME: projectName, - }, - }); - - // Pull project settings - spinner.message("Pulling project settings..."); - await execa("vercel", ["pull", "--yes"], { - stdio: "inherit", - }); - - spinner.stop("Vercel project created successfully"); - return true; - } catch (error) { - spinner.stop("Failed to set up Vercel project"); - console.error(chalk.red("Error setting up Vercel project:"), error); - return false; - } -} - -async function pushEnvironmentVariables() { - const spinner = p.spinner(); - spinner.start("Pushing environment variables to Vercel..."); - - try { - const settings = getSettings(); - const envFile = path.join(process.cwd(), settings.envFile ?? ".env"); - - if (!fs.existsSync(envFile)) { - spinner.stop("No environment file found"); - return true; - } - - const envContent = await fs.readFile(envFile, "utf-8"); - const envVars = envContent - .split("\n") - .filter((line) => line.trim() && !line.startsWith("#")) - .map((line) => { - const [key, ...valueParts] = line.split("="); - if (!key) { - return null; - } - const value = valueParts.join("="); // Rejoin in case value contains = - return { key: key.trim(), value: value.trim() }; - }) - .filter((item): item is { key: string; value: string } => item !== null); - - if (state.debug) { - spinner.stop(); - console.log("\nDebug: Parsed environment variables:"); - for (const { key, value } of envVars) { - console.log(` ${key}=${value.slice(0, 3)}...`); - } - spinner.start("Pushing environment variables to Vercel..."); - } - - let failed = 0; - const total = envVars.length; - - for (let i = 0; i < total; i++) { - const envVar = envVars[i]; - if (!envVar) { - continue; - } - const { key, value } = envVar; - spinner.message(`Pushing environment variables to Vercel... (${i + 1}/${total})`); - - try { - if (state.debug) { - console.log(`\nDebug: Attempting to add ${key} to Vercel...`); - } - - const result = await execa("vercel", ["env", "add", key, "production"], { - input: value, - stdio: "pipe", - reject: false, - }); - - if (state.debug) { - console.log(`Debug: Command exit code: ${result.exitCode}`); - if (result.stdout) { - console.log("Debug: stdout:", result.stdout); - } - if (result.stderr) { - console.log("Debug: stderr:", result.stderr); - } - } - - if (result.exitCode !== 0) { - throw new Error(`Command failed with exit code ${result.exitCode}`); - } - } catch (error) { - failed++; - if (state.debug) { - console.error(chalk.yellow(`\nDebug: Failed to add ${key}`)); - console.error("Debug: Full error:", error); - } - } - } - - if (failed > 0) { - spinner.stop(chalk.yellow(`Environment variables pushed with ${failed} failures`)); - } else { - spinner.stop("Environment variables pushed successfully"); - } - return failed < total; - } catch (error) { - spinner.stop("Failed to push environment variables"); - if (state.debug) { - console.error("\nDebug: Top-level error in pushEnvironmentVariables:"); - console.error(error); - } - return false; - } -} - -interface VercelProjectConfig { - projectId: string; - settings?: { - nodeVersion?: string; - }; - [key: string]: unknown; -} - -async function ensureCorrectNodeVersion() { - const nodeVersion = process.version.replace(VERSION_PREFIX_REGEX, ""); - const majorVersion = nodeVersion.split(".")[0]; - - try { - const projectJsonPath = ".vercel/project.json"; - if (!fs.existsSync(projectJsonPath)) { - if (state.debug) { - console.log("Debug: No project.json found"); - } - return false; - } - - const projectConfig = (await fs.readJSON(projectJsonPath)) as VercelProjectConfig; - if (state.debug) { - console.log("Debug: Current project config:", projectConfig); - } - - // Update the Node.js version - projectConfig.settings = { - ...projectConfig.settings, - nodeVersion: `${majorVersion}.x`, - }; - - await fs.writeJSON(projectJsonPath, projectConfig, { spaces: 2 }); - if (state.debug) { - console.log(`Debug: Updated Node.js version to ${majorVersion}.x`); - } - return true; - } catch (error) { - if (state.debug) { - console.error("Debug: Failed to update Node.js version:", error); - } - return false; - } -} - -async function checkVercelLogin(): Promise { - try { - const result = await execa("vercel", ["whoami"], { - stdio: "pipe", - reject: false, - }); - - if (state.debug) { - console.log("\nDebug: Vercel whoami result:", result); - } - - return result.exitCode === 0; - } catch (error) { - if (state.debug) { - console.error("Debug: Error checking Vercel login status:", error); - } - return false; - } -} - -async function loginToVercel(): Promise { - console.log(chalk.blue("\nYou need to log in to Vercel first.")); - - try { - await execa("vercel", ["login"], { - stdio: "inherit", - }); - return true; - } catch (error) { - console.error(chalk.red("\nFailed to log in to Vercel:"), error); - return false; - } -} - -export async function runDeploy() { - if (state.debug) { - console.log("Running deploy..."); - } - - // Check if Vercel CLI is installed - const hasVercelCLI = await checkVercelCLI(); - - if (!hasVercelCLI) { - const installed = await installVercelCLI(); - if (!installed) { - console.log(chalk.red("\nFailed to install Vercel CLI. Please install it manually using:")); - console.log(chalk.blue("\n npm install -g vercel")); - return; - } - } - - // Check if user is logged in - const isLoggedIn = await checkVercelLogin(); - if (!isLoggedIn) { - const loginSuccessful = await loginToVercel(); - if (!loginSuccessful) { - console.log(chalk.red("\nFailed to log in to Vercel. Please try again.")); - return; - } - } - - // Check if project is set up with Vercel - const hasVercelProject = await checkVercelProject(); - - if (!hasVercelProject) { - console.log(chalk.blue("\nSetting up new Vercel project...")); - const setup = await setupVercelProject(); - if (!setup) { - console.log(chalk.red("\nFailed to set up Vercel project automatically.")); - return; - } - - const envPushed = await pushEnvironmentVariables(); - if (!envPushed) { - console.log(chalk.red("\nFailed to push environment variables. Aborting deployment.")); - return; - } - } - - // Pull latest project settings - console.log(chalk.blue("\nPulling latest project settings...")); - try { - await execa("vercel", ["pull", "--yes"], { - stdio: "inherit", - }); - } catch (error) { - console.error(chalk.red("\nFailed to pull project settings:"), error); - return; - } - - // Ensure correct Node.js version is set - if (!(await ensureCorrectNodeVersion())) { - console.error(chalk.red("\nFailed to set Node.js version. Continuing anyway...")); - } - - if (state.localBuild) { - // Build locally for Vercel - console.log(chalk.blue("\nPreparing local build for Vercel...")); - try { - const result = await execa("vercel", ["build"], { - stdio: "inherit", - reject: false, - }); - if (result.exitCode === 0) { - console.log(chalk.green("\nโœ“ Local build successful!")); - } else { - console.error(chalk.red("\nโœ– Local build failed")); - console.log(chalk.yellow("Fix the errors above and then try again.")); - return; - } - } catch (error) { - console.error(chalk.red("\nVercel build failed:"), error); - return; - } - - // Deploy the pre-built project - console.log(chalk.blue("\nDeploying to Vercel...")); - - const result = await execa("vercel", ["deploy", "--prebuilt", "--yes"], { - stdio: "inherit", - reject: false, - }); - if (result.exitCode === 0) { - console.log(chalk.green("\nโœ“ Deployment successful!")); - } - } else { - // Deploy and build on Vercel - console.log(chalk.blue("\nDeploying to Vercel...")); - try { - const result = await execa("vercel", ["deploy", "--yes"], { - stdio: "inherit", - reject: false, - }); - if (result.exitCode === 0) { - console.log(chalk.green("\nโœ“ Deployment successful!")); - } else { - const pkgManager = getUserPkgManager(); - const runCmd = pkgManager === "npm" ? "npm run" : pkgManager; - console.error(chalk.red("\nโœ– Deployment failed")); - - console.log(chalk.yellow("\nTroubleshooting Tips:")); - console.log(chalk.dim("You can check for most errors before deploying for a faster iteration cycle")); - console.log( - `${chalk.dim("Run")} ${runCmd} tsc ${chalk.dim("to check for TypeScript errors (most common build errors)")}`, - ); - console.log(`${chalk.dim("Run")} ${runCmd} build ${chalk.dim("to run the full production build locally")}`); - } - } catch { - // This catch block should rarely be hit since we're using reject: false - return; - } - } -} - -export const makeDeployCommand = () => { - const deployCommand = new Command("deploy") - .description("Deploy your ProofKit application to Vercel") - .addOption(nonInteractiveOption) - .addOption(debugOption) - .addOption(new Option("--local-build", "Build locally before deploying")) - .action(runDeploy); - - deployCommand.hook("preAction", (thisCommand) => { - initProgramState(thisCommand.opts()); - state.baseCommand = "deploy"; - ensureProofKitProject({ commandName: "deploy" }); - }); - - return deployCommand; -}; diff --git a/packages/cli/src/cli/fmdapi.ts b/packages/cli/src/cli/fmdapi.ts deleted file mode 100644 index cb252b8a..00000000 --- a/packages/cli/src/cli/fmdapi.ts +++ /dev/null @@ -1,57 +0,0 @@ -import DataApi, { type clientTypes, OttoAdapter, type OttoAPIKey } from "@proofkit/fmdapi"; - -export async function getLayouts({ - dataApiKey, - fmFile, - server, -}: { - dataApiKey: OttoAPIKey; - fmFile: string; - server: string; -}) { - const DapiClient = DataApi({ - adapter: new OttoAdapter({ - auth: { apiKey: dataApiKey }, - db: fmFile, - server, - }), - layout: "", - }); - - const layoutsResp = await DapiClient.layouts(); - - const layouts = transformLayoutList(layoutsResp.layouts); - - return layouts; -} - -function getAllLayoutNames(layout: clientTypes.LayoutOrFolder): string[] { - if ("isFolder" in layout) { - return (layout.folderLayoutNames ?? []).flatMap(getAllLayoutNames); - } - return [layout.name]; -} - -export const commonFileMakerLayoutPrefixes = ["API_", "API ", "dapi_", "dapi"]; - -export function transformLayoutList(layouts: clientTypes.LayoutOrFolder[]): string[] { - const flatList = layouts.flatMap(getAllLayoutNames); - - // sort the list so that any values that begin with one of the prefixes are at the top - - const sortedList = flatList.sort((a, b) => { - const aPrefix = commonFileMakerLayoutPrefixes.find((prefix) => a.startsWith(prefix)); - const bPrefix = commonFileMakerLayoutPrefixes.find((prefix) => b.startsWith(prefix)); - if (aPrefix && bPrefix) { - return a.localeCompare(b); - } - if (aPrefix) { - return -1; - } - if (bPrefix) { - return 1; - } - return a.localeCompare(b); - }); - return sortedList; -} diff --git a/packages/cli/src/cli/init.ts b/packages/cli/src/cli/init.ts deleted file mode 100644 index e7e0d46c..00000000 --- a/packages/cli/src/cli/init.ts +++ /dev/null @@ -1,479 +0,0 @@ -import path from "node:path"; -import { Command } from "commander"; -import { execa } from "execa"; -import fs from "fs-extra"; -import type { PackageJson } from "type-fest"; - -import { DEFAULT_APP_NAME, NODE_RUNTIME_VERSION } from "~/consts.js"; -import { createPnpmWorkspaceFileContent } from "~/core/planInit.js"; -import { addAuth } from "~/generators/auth.js"; -import { runCodegenCommand } from "~/generators/fmdapi.js"; -import { debugOption, nonInteractiveOption } from "~/globalOptions.js"; -import { createBareProject } from "~/helpers/createProject.js"; -import { initializeGit } from "~/helpers/git.js"; -import { installDependencies } from "~/helpers/installDependencies.js"; -import { getIntentInstallCommand } from "~/helpers/intent.js"; -import { logNextSteps } from "~/helpers/logNextSteps.js"; -import { setImportAlias } from "~/helpers/setImportAlias.js"; -import { - getBrowserOxlintConfig, - getHuskyPreCommitHook, - getUltraciteInitCommand, - getWebViewerOxlintConfig, -} from "~/helpers/ultracite.js"; -import { buildPkgInstallerMap } from "~/installers/index.js"; -import { initProgramState, isNonInteractiveMode, state } from "~/state.js"; -import { getVersion } from "~/utils/getProofKitVersion.js"; -import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; -import { logger } from "~/utils/logger.js"; -import { parseNameAndPath } from "~/utils/parseNameAndPath.js"; -import { type Settings, setSettings } from "~/utils/parseSettings.js"; -import { formatPackageManagerCommand, parseCommandString } from "~/utils/projectFiles.js"; -import { validateAppName } from "~/utils/validateAppName.js"; -import { promptForFileMakerDataSource } from "./add/data-source/filemaker.js"; -import { select, text } from "./prompts.js"; -import { abortIfCancel } from "./utils.js"; - -interface CliFlags { - noGit: boolean; - noInstall: boolean; - force: boolean; - default: boolean; - importAlias: string; - server?: string; - adminApiKey?: string; - fileName: string; - layoutName: string; - schemaName: string; - dataApiKey: string; - fmServerURL: string; - auth: "none" | "next-auth" | "clerk"; - dataSource?: "filemaker" | "none" | "supabase"; - /** @internal Used in CI. */ - CI: boolean; - /** @internal Used in non-interactive mode. */ - nonInteractive?: boolean; - /** @internal Used in CI. */ - tailwind: boolean; - /** @internal Used in CI. */ - trpc: boolean; - /** @internal Used in CI. */ - prisma: boolean; - /** @internal Used in CI. */ - drizzle: boolean; - /** @internal Used in CI. */ - appRouter: boolean; -} - -const defaultOptions: CliFlags = { - noGit: false, - noInstall: false, - force: false, - default: false, - CI: false, - tailwind: false, - trpc: false, - prisma: false, - drizzle: false, - importAlias: "~/", - appRouter: false, - auth: "none", - server: undefined, - fileName: "", - layoutName: "", - schemaName: "", - dataApiKey: "", - fmServerURL: "", - dataSource: undefined, -}; - -export const makeInitCommand = () => { - const initCommand = new Command("init") - .description("Create a new project with ProofKit") - .argument("[dir]", "The name of the application, as well as the name of the directory to create") - .option("--appType [type]", "The type of app to create", undefined) - .option("--server [url]", "The URL of your FileMaker Server", undefined) - .option("--adminApiKey [key]", "Admin API key for OttoFMS. If provided, will skip login prompt", undefined) - .option("--fileName [name]", "The name of the FileMaker file to use for the web app", undefined) - .option("--layoutName [name]", "The name of the FileMaker layout to use for the web app", undefined) - .option("--schemaName [name]", "The name for the generated layout client in your schemas", undefined) - .option("--dataApiKey [key]", "The API key to use for the FileMaker Data API", undefined) - .option("--auth [type]", "The authentication provider to use for the web app", undefined) - .option("--dataSource [type]", "The data source to use for the web app (filemaker or none)", undefined) - .option("--noGit", "Explicitly tell the CLI to not initialize a new git repo in the project", false) - .option("--noInstall", "Explicitly tell the CLI to not run the package manager's install command", false) - .option("-f, --force", "Force overwrite target directory when it already contains files", false) - .addOption(nonInteractiveOption) - .addOption(debugOption) - .action(runInit); - - initCommand.hook("preAction", (cmd) => { - initProgramState(cmd.opts()); - state.baseCommand = "init"; - }); - - return initCommand; -}; - -async function askForAuth({ projectDir }: { projectDir: string }) { - const authType = "none" as "none" | "clerk" | "fmaddon"; - if (authType === "clerk") { - await addAuth({ - options: { type: "clerk" }, - projectDir, - noInstall: true, - }); - } else if (authType === "fmaddon") { - await addAuth({ - options: { type: "fmaddon" }, - projectDir, - noInstall: true, - }); - } -} - -type ProofKitPackageJSON = PackageJson & { - proofkitMetadata?: { - initVersion: string; - }; - devEngines?: { - packageManager: { - name: string; - version: string; - onFail: "download"; - }; - runtime: { - name: "node"; - version: string; - onFail: "download"; - }; - }; - engines?: { - node: string; - }; -}; - -const missingTypegenCommandPatterns = [ - /ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL[\s\S]*Command\s+["'`]typegen["'`]\s+not found/i, - /Command\s+["'`]typegen["'`]\s+not found/i, - /Missing script:\s*["'`]typegen["'`]/i, - /Script not found\s*["'`]typegen["'`]/i, -]; - -function getErrorMessage(error: unknown): string { - if (error instanceof Error) { - return error.message; - } - return String(error); -} - -function createErrorWithCause(message: string, cause: Error): Error { - const wrapped = new Error(message) as Error & { cause?: Error }; - wrapped.cause = cause; - return wrapped; -} - -export function isMissingTypegenCommandError(error: unknown): boolean { - const message = getErrorMessage(error); - return missingTypegenCommandPatterns.some((pattern) => pattern.test(message)); -} - -export function createPostInitGenerationError({ - error, - appType, - projectDir, -}: { - error: unknown; - appType: "browser" | "webviewer"; - projectDir: string; -}) { - const rootError = error instanceof Error ? error : new Error(getErrorMessage(error)); - - if (appType === "browser" && isMissingTypegenCommandError(error)) { - return createErrorWithCause( - [ - "Post-init generation failed after scaffolding.", - `Project created at: ${projectDir}`, - "Root cause: a `typegen` package command was invoked, but browser scaffolds do not define that script.", - "Continue using the generated project, then run `npx @proofkit/typegen` later after FileMaker setup is complete.", - ].join("\n"), - rootError, - ); - } - - return createErrorWithCause( - [ - "Post-init generation failed after scaffolding.", - `Project created at: ${projectDir}`, - "Retry `npx @proofkit/typegen` from inside the project once FileMaker settings and connectivity are valid.", - `Underlying error: ${getErrorMessage(error)}`, - ].join("\n"), - rootError, - ); -} - -export const runInit = async (name?: string, opts?: CliFlags) => { - const pkgManager = getUserPkgManager(); - const cliOptions = opts ?? defaultOptions; - const nonInteractive = isNonInteractiveMode(); - const noInstall = cliOptions.noInstall ?? (opts as { install?: boolean } | undefined)?.install === false; - const noGit = cliOptions.noGit ?? (opts as { git?: boolean } | undefined)?.git === false; - state.ui = "shadcn"; - - let projectName = name; - if (!projectName) { - if (nonInteractive) { - throw new Error("Project name is required in non-interactive mode."); - } - projectName = abortIfCancel( - await text({ - message: "What will your project be called?", - defaultValue: DEFAULT_APP_NAME, - validate: validateAppName, - }), - ).toString(); - } - - const appNameValidation = validateAppName(projectName); - if (appNameValidation) { - throw new Error(appNameValidation); - } - - const hasExplicitFileMakerInputs = Boolean( - cliOptions.server || - cliOptions.adminApiKey || - cliOptions.dataApiKey || - cliOptions.fileName || - cliOptions.layoutName || - cliOptions.schemaName, - ); - const hasPartialFileMakerSchemaInputs = Boolean(cliOptions.layoutName) !== Boolean(cliOptions.schemaName); - - if (!state.appType) { - state.appType = nonInteractive - ? "browser" - : (abortIfCancel( - await select({ - message: "What kind of app do you want to build?", - options: [ - { - value: "browser", - label: "Web App for Browsers", - hint: "Uses Next.js, will require hosting", - }, - { - value: "webviewer", - label: "FileMaker Web Viewer (beta)", - hint: "Uses Vite, can be embedded in FileMaker or hosted", - }, - ], - }), - ) as "browser" | "webviewer"); - } - - if (nonInteractive && hasPartialFileMakerSchemaInputs) { - throw new Error("Both --layoutName and --schemaName must be provided together."); - } - - if (nonInteractive && hasExplicitFileMakerInputs) { - const resolvedDataSourceForValidation = - state.appType === "webviewer" - ? (cliOptions.dataSource ?? (cliOptions.server ? "filemaker" : "none")) - : (cliOptions.dataSource ?? "none"); - - if (resolvedDataSourceForValidation !== "filemaker") { - throw new Error("FileMaker flags require --dataSource filemaker in non-interactive mode."); - } - } - - const usePackages = buildPkgInstallerMap(); - - // e.g. dir/@mono/app returns ["@mono/app", "dir/app"] - const [scopedAppName, appDir] = parseNameAndPath(projectName); - - const projectDir = await createBareProject({ - projectName: appDir, - scopedAppName, - packages: usePackages, - noInstall, - force: cliOptions.force, - appRouter: cliOptions.appRouter, - }); - setImportAlias(projectDir, "@/"); - - // Write name to package.json - const pkgJson = fs.readJSONSync(path.join(projectDir, "package.json")) as ProofKitPackageJSON; - pkgJson.name = scopedAppName; - pkgJson.proofkitMetadata = { initVersion: getVersion() }; - - // ? Bun doesn't support this field (yet) - let pkgManagerVersion: string | undefined; - if (pkgManager !== "bun") { - const { stdout } = await execa(pkgManager, ["-v"], { - cwd: projectDir, - }); - pkgManagerVersion = stdout.trim(); - pkgJson.packageManager = undefined; - pkgJson.devEngines = { - packageManager: { - name: pkgManager, - version: pkgManagerVersion, - onFail: "download", - }, - runtime: { - name: "node", - version: NODE_RUNTIME_VERSION, - onFail: "download", - }, - }; - } - pkgJson.engines = { - node: NODE_RUNTIME_VERSION, - }; - - fs.writeJSONSync(path.join(projectDir, "package.json"), pkgJson, { - spaces: 2, - }); - if (pkgManager === "pnpm") { - fs.writeFileSync( - path.join(projectDir, "pnpm-workspace.yaml"), - createPnpmWorkspaceFileContent(state.appType ?? "browser"), - "utf8", - ); - } - - // Ensure proofkit.json exists with shadcn settings - const initialSettings: Settings = { - appType: state.appType ?? "browser", - ui: "shadcn", - envFile: ".env", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], - }; - setSettings(initialSettings); - - // for webviewer apps FM is required, so don't ask - let dataSource = - state.appType === "webviewer" - ? (cliOptions.dataSource ?? (nonInteractive && !cliOptions.server ? "none" : "filemaker")) - : (cliOptions.dataSource ?? (nonInteractive ? "none" : undefined)); - if (!dataSource) { - dataSource = abortIfCancel( - await select({ - message: "Do you want to connect to a FileMaker Database now?", - options: [ - { - value: "filemaker", - label: "Yes", - hint: "Requires OttoFMS and Admin Server credentials", - }, - // { value: "supabase", label: "Supabase" }, - { - value: "none", - label: "No", - hint: "You'll be able to add a new data source later", - }, - ], - }), - ) as "filemaker" | "none" | "supabase"; - } - - if (dataSource === "filemaker") { - // later will split this flow to ask for which kind of data souce, but for now it's just FM - await promptForFileMakerDataSource({ - projectDir, - name: "filemaker", - adminApiKey: cliOptions.adminApiKey, - dataApiKey: cliOptions.dataApiKey, - server: cliOptions.server, - fileName: cliOptions.fileName, - layoutName: cliOptions.layoutName, - schemaName: cliOptions.schemaName, - }); - } else if (dataSource === "supabase") { - // TODO: add supabase - } - - await askForAuth({ projectDir }); - - if (!noInstall) { - await installDependencies({ projectDir }); - } - - const ultraciteCommand = getUltraciteInitCommand({ - appType: state.appType ?? "browser", - packageManager: pkgManager, - skipInstall: noInstall, - }); - await execa(ultraciteCommand.command, ultraciteCommand.args, { - cwd: projectDir, - stdio: "pipe", - }); - const oxlintConfigContent = - (state.appType ?? "browser") === "browser" ? getBrowserOxlintConfig() : getWebViewerOxlintConfig(); - fs.writeFileSync(path.join(projectDir, "oxlint.config.ts"), oxlintConfigContent, "utf8"); - fs.writeFileSync(path.join(projectDir, ".husky/pre-commit"), getHuskyPreCommitHook(), "utf8"); - const intentCommand = getIntentInstallCommand(pkgManager); - await execa(intentCommand.command, intentCommand.args, { - cwd: projectDir, - stdio: "pipe", - }); - - if (dataSource === "filemaker") { - const shouldRunInitialCodegen = state.appType === "webviewer" && !(nonInteractive && !hasExplicitFileMakerInputs); - - if (shouldRunInitialCodegen) { - try { - await runCodegenCommand(); - } catch (error) { - throw createPostInitGenerationError({ - error, - appType: state.appType ?? "browser", - projectDir, - }); - } - } - } - - if (!noInstall) { - const fixCommandString = formatPackageManagerCommand(pkgManager, "fix"); - const [fixCommand, ...fixArgs] = parseCommandString(fixCommandString); - if (!fixCommand) { - throw new Error(`Unable to resolve fix command for ${pkgManager}.`); - } - await execa(fixCommand, fixArgs, { - cwd: projectDir, - stdio: "pipe", - }).catch((error: unknown) => { - if (state.debug) { - logger.warn(`Fix command failed; continuing. packageManager=${pkgManager} command=${fixCommandString}`); - logger.error(error); - } - }); - - const lintCommandString = formatPackageManagerCommand(pkgManager, "lint"); - const [lintCommand, ...lintArgs] = parseCommandString(lintCommandString); - if (!lintCommand) { - throw new Error(`Unable to resolve lint command for ${pkgManager}.`); - } - await execa(lintCommand, lintArgs, { - cwd: projectDir, - stdio: "pipe", - }).catch((error: unknown) => { - logger.warn(`Lint did not succeed; continuing setup. packageManager=${pkgManager} command=${lintCommandString}`); - if (state.debug) { - logger.error(error); - } - }); - } - - if (!noGit) { - await initializeGit(projectDir); - } - - logNextSteps({ - projectName: appDir, - noInstall, - }); -}; diff --git a/packages/cli/src/cli/menu.ts b/packages/cli/src/cli/menu.ts deleted file mode 100644 index 8a0ce42e..00000000 --- a/packages/cli/src/cli/menu.ts +++ /dev/null @@ -1,96 +0,0 @@ -import chalk from "chalk"; -import { Effect } from "effect"; -import open from "open"; -import { confirm, log, select } from "~/cli/prompts.js"; - -import { DOCS_URL } from "~/consts.js"; -import { runDoctor } from "~/core/doctor.js"; -import { runPrompt } from "~/core/prompt.js"; -import { makeLiveLayer } from "~/services/live.js"; -import { checkForAvailableUpgrades, runAllAvailableUpgrades } from "~/upgrades/index.js"; -import { resolveNonInteractiveMode } from "~/utils/nonInteractive.js"; -import { runDeploy } from "./deploy/index.js"; -import { abortIfCancel } from "./utils.js"; - -function getMenuRuntime() { - return makeLiveLayer({ - cwd: process.cwd(), - debug: false, - nonInteractive: resolveNonInteractiveMode({ - stdinIsTTY: process.stdin?.isTTY, - stdoutIsTTY: process.stdout?.isTTY, - }), - }); -} - -export const runMenu = async () => { - const upgrades = checkForAvailableUpgrades(); - - if (upgrades.length > 0) { - log.info( - `${chalk.yellow("There are upgrades available for your ProofKit project")}\n${upgrades - .map((upgrade) => `- ${upgrade.title}`) - .join("\n")}`, - ); - - const shouldRunUpgrades = abortIfCancel( - await confirm({ - message: "Would you like to run them now?", - initialValue: true, - }), - ); - - if (shouldRunUpgrades) { - await runAllAvailableUpgrades(); - log.success(chalk.green("Successfully ran all upgrades")); - } else { - log.info(`You can apply the upgrades later by running ${chalk.cyan("proofkit upgrade")}`); - } - } - - const menuChoice = abortIfCancel( - await select({ - message: "What would you like to do?", - options: [ - { - label: "Doctor", - value: "doctor", - hint: "Inspect project health and get exact next steps", - }, - { - label: "Prompt", - value: "prompt", - hint: "Reserved AI-agent workflow entrypoint", - }, - { - label: "Deploy", - value: "deploy", - hint: "Deploy your app to Vercel", - }, - { - label: "View Documentation", - value: "docs", - hint: "Open ProofKit documentation", - }, - ], - }), - ); - - switch (menuChoice) { - case "doctor": - await Effect.runPromise(getMenuRuntime()(runDoctor)); - break; - case "prompt": - await Effect.runPromise(getMenuRuntime()(runPrompt)); - break; - case "docs": - log.info(`Opening ${chalk.cyan(DOCS_URL)} in your browser...`); - await open(DOCS_URL); - break; - case "deploy": - await runDeploy(); - break; - default: - throw new Error(`Unknown menu choice: ${menuChoice}`); - } -}; diff --git a/packages/cli/src/cli/ottofms.ts b/packages/cli/src/cli/ottofms.ts deleted file mode 100644 index e6bb71a9..00000000 --- a/packages/cli/src/cli/ottofms.ts +++ /dev/null @@ -1,277 +0,0 @@ -import axios, { AxiosError } from "axios"; -import chalk from "chalk"; -import open from "open"; -import randomstring from "randomstring"; -import { z } from "zod/v4"; - -import * as clack from "~/cli/prompts.js"; -import { abortIfCancel } from "./utils.js"; - -interface WizardResponse { - token: string; -} -export async function getOttoFMSToken({ url }: { url: URL }): Promise<{ token: string }> { - // generate a random string - const hash = randomstring.generate({ length: 18, charset: "alphanumeric" }); - - const loginUrl = new URL(`/otto/wizard/${hash}`, url.origin); - - const urlToOpen = loginUrl.toString(); - clack.log.info( - `${chalk.bold( - `If the browser window didn't open automatically, please open the following link to login into your OttoFMS server:`, - )}\n\n${chalk.cyan(urlToOpen)}`, - ); - - open(loginUrl.toString()).catch(() => { - // Ignore errors from open() - the user can manually open the URL - }); - - const loginSpinner = clack.spinner(); - - loginSpinner.start("Waiting for you to log in using the link above"); - - const data = await new Promise((resolve, reject) => { - let settled = false; - const pollingInterval = setInterval(() => { - axios - .get<{ response: WizardResponse }>(`${url.origin}/otto/api/cli/checkHash/${hash}`, { - headers: { - "Accept-Encoding": "deflate", - }, - }) - .then((result) => { - if (settled) { - return; - } - settled = true; - resolve(result.data.response); - clearTimeout(timeout); - clearInterval(pollingInterval); - axios - .delete(`${url.origin}/otto/api/cli/checkHash/${hash}`, { - headers: { - "Accept-Encoding": "deflate", - }, - }) - .catch(() => { - // Ignore cleanup errors - }); - }) - .catch(() => { - // noop - just try again - }); - }, 500); - - const timeout = setTimeout(() => { - if (settled) { - return; - } - settled = true; - clearInterval(pollingInterval); - clearTimeout(timeout); - loginSpinner.stop("Login timed out. No worries - it happens to the best of us."); - reject(new Error("Login timed out")); - }, 180_000); // 3 minutes - }); - // clack.log.info(`Token: ${JSON.stringify(data)}`); - - loginSpinner.stop("Login complete."); - - return data; -} - -interface ListFilesResponse { - response: { - databases: { - clients: number; - decryptHint: string; - enabledExtPrivileges: string[]; - filename: string; - folder: string; - hasSavedDecryptKey: boolean; - id: string; - isEncrypted: boolean; - size: number; - status: string; - }[]; - }; -} - -export async function listFiles({ url, token }: { url: URL; token: string }) { - const response = await axios.get(`${url.origin}/otto/fmi/admin/api/v2/databases`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - return response.data.response.databases; -} - -interface ListAPIKeysResponse { - response: { - "api-keys": { - id: number; - key: string; - token: string; - user: string; - database: string; - label: string; - created_at: string; - updated_at: string; - }[]; - }; -} - -export async function listAPIKeys({ url, token }: { url: URL; token: string }) { - const response = await axios.get(`${url.origin}/otto/api/api-key`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - return response.data.response["api-keys"]; -} - -interface CreateAPIKeyResponse { - response: { - key: string; - token: string; - }; -} -export async function createDataAPIKey({ url, filename }: { url: URL; filename: string }) { - clack.log.info( - `${chalk.cyan("Creating a Data API Key")}\nEnter FileMaker credentials for ${chalk.bold(filename)}.\n${chalk.dim("The account must have the fmrest extended privilege enabled.")}`, - ); - - while (true) { - const username = abortIfCancel( - await clack.text({ - message: `Enter the account name for ${chalk.bold(filename)}`, - }), - ); - - const password = abortIfCancel( - await clack.password({ - message: `Enter the password for ${chalk.bold(username)}`, - }), - ); - - try { - const response = await createDataAPIKeyWithCredentials({ - url, - filename, - username, - password, - }); - - return response; - } catch (error) { - if (error instanceof AxiosError) { - const respMsg = - error.response?.data && "messages" in error.response.data - ? (error.response.data as { messages?: { text?: string }[] }).messages?.[0]?.text - : undefined; - - clack.log.error( - `${chalk.red("Error creating Data API key:")} ${respMsg ?? `Error code ${error.response?.status}`} -${chalk.dim( - error.response?.status === 400 && - `Common reasons this might happen: -- The provided credentials are incorrect. -- The account does not have the fmrest extended privilege enabled. - -You may also want to try to create an API directly in the OttoFMS dashboard: -${url.origin}/otto/app/api-keys`, -)} - `, - ); - } else { - clack.log.error(`${chalk.red("Error creating Data API key:")} Unknown error`); - } - const tryAgain = abortIfCancel( - await clack.confirm({ - message: "Do you want to try and enter credentials again?", - }), - ); - if (!tryAgain) { - throw new Error("User cancelled"); - } - } - } -} - -export async function createDataAPIKeyWithCredentials({ - url, - filename, - username, - password, -}: { - url: URL; - filename: string; - username: string; - password: string; -}) { - const response = await axios.post(`${url.origin}/otto/api/api-key/create-only`, { - database: filename, - label: "For FM Web App", - user: username, - pass: password, - }); - - return { apiKey: response.data.response.key }; -} - -export async function startDeployment({ payload, url, token }: { payload: unknown; url: URL; token: string }) { - const responseSchema = z.object({ - response: z.object({ - started: z.boolean(), - batchId: z.number(), - subDeploymentIds: z.array(z.number()), - }), - messages: z.array(z.object({ code: z.number(), text: z.string() })), - }); - - const response = await axios - .post(`${url.origin}/otto/api/deployment`, payload, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - .catch((error) => { - console.error(error.response.data); - throw error; - }); - - return responseSchema.parse(response.data); -} - -export async function getDeploymentStatus({ - url, - token, - deploymentId, -}: { - url: URL; - token: string; - deploymentId: number; -}) { - const schema = z.object({ - response: z.object({ - id: z.number(), - status: z.enum(["queued", "running", "scheduled", "complete", "aborted", "unknown"]), - running: z.coerce.boolean(), - created_at: z.string(), - started_at: z.string(), - updated_at: z.string(), - }), - messages: z.array(z.object({ code: z.number(), text: z.string() })), - }); - - const response = await axios.get(`${url.origin}/otto/api/deployment/${deploymentId}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - return schema.parse(response.data); -} diff --git a/packages/cli/src/cli/prompts.ts b/packages/cli/src/cli/prompts.ts deleted file mode 100644 index ea4bc1b2..00000000 --- a/packages/cli/src/cli/prompts.ts +++ /dev/null @@ -1,186 +0,0 @@ -import * as clack from "@clack/prompts"; -import { - checkbox as inquirerCheckbox, - confirm as inquirerConfirm, - input as inquirerInput, - password as inquirerPassword, - search as inquirerSearch, - select as inquirerSelect, -} from "@inquirer/prompts"; - -const CANCEL_SYMBOL = Symbol.for("@proofkit/cli/prompt-cancelled"); - -export const intro = clack.intro; -export const outro = clack.outro; -export const note = clack.note; -export const log = clack.log; -export const spinner = clack.spinner; -export const cancel = clack.cancel; - -export interface PromptOption { - value: T; - label: string; - hint?: string; - disabled?: boolean | string; -} - -export interface SearchPromptOption extends PromptOption { - keywords?: readonly string[]; -} - -function normalizeValidate( - validate: ((value: string) => string | undefined) | undefined, -): ((value: string) => string | boolean) | undefined { - if (!validate) { - return undefined; - } - - return (value: string) => validate(value) ?? true; -} - -function normalizeDisabledMessage(value: boolean | string | undefined) { - if (typeof value === "string") { - return value; - } - return value ? true : undefined; -} - -function isPromptCancel(error: unknown) { - return error instanceof Error && error.name === "ExitPromptError"; -} - -function withCancelSentinel(fn: () => Promise): Promise { - return fn().catch((error: unknown) => { - if (isPromptCancel(error)) { - return CANCEL_SYMBOL; - } - throw error; - }); -} - -export function isCancel(value: unknown): value is symbol { - return value === CANCEL_SYMBOL || clack.isCancel(value); -} - -function matchesSearch(option: SearchPromptOption, query: string) { - const haystack = [option.label, option.hint ?? "", ...(option.keywords ?? [])].join(" ").toLowerCase(); - return haystack.includes(query.trim().toLowerCase()); -} - -export function filterSearchOptions( - options: readonly SearchPromptOption[], - query: string | undefined, -) { - const term = query?.trim(); - if (!term) { - return options; - } - - return options.filter((option) => matchesSearch(option, term)); -} - -export function text(options: { - message: string; - defaultValue?: string; - validate?: (value: string) => string | undefined; -}) { - return withCancelSentinel(() => - inquirerInput({ - message: options.message, - default: options.defaultValue, - validate: normalizeValidate(options.validate), - }), - ); -} - -export function password(options: { message: string; validate?: (value: string) => string | undefined }) { - return withCancelSentinel(() => - inquirerPassword({ - message: options.message, - validate: normalizeValidate(options.validate), - }), - ); -} - -export function confirm(options: { message: string; initialValue?: boolean }) { - return withCancelSentinel( - () => - inquirerConfirm({ - message: options.message, - default: options.initialValue, - }) as Promise, - ); -} - -export function select(options: { - message: string; - options: PromptOption[]; - maxItems?: number; - initialValue?: T; -}) { - return withCancelSentinel(() => - inquirerSelect({ - message: options.message, - pageSize: options.maxItems ?? 10, - default: options.initialValue, - choices: options.options.map((option) => ({ - value: option.value, - name: option.label, - description: option.hint, - disabled: normalizeDisabledMessage(option.disabled), - })), - }), - ); -} - -export function searchSelect(options: { - message: string; - emptyMessage?: string; - options: SearchPromptOption[]; -}) { - return withCancelSentinel(() => - inquirerSearch({ - message: options.message, - pageSize: 10, - source: (input) => { - const filtered = filterSearchOptions(options.options, input); - if (filtered.length === 0) { - return [ - { - value: "__no_matches__" as T, - name: options.emptyMessage ?? "No matches found. Keep typing to refine your search.", - disabled: options.emptyMessage ?? "No matches found", - }, - ]; - } - - return filtered.map((option) => ({ - value: option.value, - name: option.label, - description: option.hint, - disabled: normalizeDisabledMessage(option.disabled), - })); - }, - }), - ); -} - -export function multiSearchSelect(options: { - message: string; - options: SearchPromptOption[]; - required?: boolean; -}) { - return withCancelSentinel(() => - inquirerCheckbox({ - message: options.message, - pageSize: 10, - required: options.required, - choices: options.options.map((option) => ({ - value: option.value, - name: option.label, - description: option.hint, - disabled: normalizeDisabledMessage(option.disabled), - })), - }), - ); -} diff --git a/packages/cli/src/cli/react-email.ts b/packages/cli/src/cli/react-email.ts deleted file mode 100644 index f0ac245a..00000000 --- a/packages/cli/src/cli/react-email.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Command, Option } from "commander"; -import * as p from "~/cli/prompts.js"; - -import { installReactEmail } from "~/installers/react-email.js"; - -export const runAddReactEmailCommand = async ({ - noInstall, - installServerFiles, -}: { - noInstall?: boolean; - installServerFiles?: boolean; -} = {}) => { - const spinner = p.spinner(); - spinner.start("Adding React Email"); - await installReactEmail({ noInstall, installServerFiles }); - spinner.stop("React Email added"); -}; - -export const makeAddReactEmailCommand = () => { - const addReactEmailCommand = new Command("react-email") - .description("Add React Email scaffolding to your project") - .addOption(new Option("--noInstall", "Do not run your package manager install command").default(false)) - .option("--installServerFiles", "Also scaffold provider-specific server email files", false) - .action((args: { noInstall?: boolean; installServerFiles?: boolean }) => runAddReactEmailCommand(args)); - - return addReactEmailCommand; -}; diff --git a/packages/cli/src/cli/remove/data-source.ts b/packages/cli/src/cli/remove/data-source.ts deleted file mode 100644 index 3118c1bd..00000000 --- a/packages/cli/src/cli/remove/data-source.ts +++ /dev/null @@ -1,152 +0,0 @@ -import path from "node:path"; -import { Command } from "commander"; -import dotenv from "dotenv"; -import fs from "fs-extra"; -import { z } from "zod/v4"; -import * as p from "~/cli/prompts.js"; -import { UserCancelledError } from "~/core/errors.js"; -import { removeFromFmschemaConfig, runCodegenCommand } from "~/generators/fmdapi.js"; -import { debugOption, nonInteractiveOption } from "~/globalOptions.js"; -import { initProgramState, isNonInteractiveMode, state } from "~/state.js"; -import { type DataSource, getSettings, setSettings } from "~/utils/parseSettings.js"; -import { abortIfCancel, ensureProofKitProject } from "../utils.js"; - -function getDataSourceInfo(source: DataSource) { - if (source.type !== "fm") { - return source.type; - } - - const envFile = path.join(state.projectDir, ".env"); - if (fs.existsSync(envFile)) { - dotenv.config({ path: envFile }); - } - - const server = process.env[source.envNames.server] || "unknown server"; - const database = process.env[source.envNames.database] || "unknown database"; - - try { - // Format the server URL to be more readable - const serverUrl = new URL(server); - const formattedServer = serverUrl.hostname; - return `${formattedServer}/${database}`; - } catch (error) { - if (state.debug) { - console.error("Error parsing server URL:", error); - } - return `${server}/${database}`; - } -} - -export const runRemoveDataSourceCommand = async (name?: string) => { - const settings = getSettings(); - - if (settings.dataSources.length === 0) { - p.note("No data sources found in your project."); - return; - } - - let dataSourceName = name; - - // If no name provided, prompt for selection - if (dataSourceName) { - // Validate that the provided name exists - const dataSourceExists = settings.dataSources.some((source) => source.name === dataSourceName); - if (!dataSourceExists) { - throw new Error(`Data source "${dataSourceName}" not found in your project.`); - } - } else { - dataSourceName = abortIfCancel( - await p.select({ - message: "Which data source do you want to remove?", - options: settings.dataSources.map((source) => { - let info = ""; - try { - info = getDataSourceInfo(source); - } catch (error) { - if (state.debug) { - console.error("Error getting data source info:", error); - } - info = "unknown connection"; - } - return { - label: `${source.name} (${info})`, - value: source.name, - }; - }), - }), - ); - } - - let confirmed = true; - if (!isNonInteractiveMode()) { - confirmed = abortIfCancel( - await p.confirm({ - message: `Are you sure you want to remove the data source "${dataSourceName}"? This will only remove it from your configuration, not replace any possible usage, which may cause TypeScript errors.`, - }), - ); - - if (!confirmed) { - throw new UserCancelledError({ message: "User aborted the operation" }); - } - } - - // Get the data source before removing it - const dataSource = settings.dataSources.find((source) => source.name === dataSourceName); - - // Remove the data source from settings - settings.dataSources = settings.dataSources.filter((source) => source.name !== dataSourceName); - - // Save the updated settings - setSettings(settings); - - if (dataSource?.type === "fm") { - // For FileMaker data sources, remove from fmschema.config.mjs - removeFromFmschemaConfig({ - dataSourceName, - }); - - if (state.debug) { - p.note("Removed schemas from fmschema.config.mjs"); - } - - // Remove the schema folder for this data source - const schemaFolderPath = path.join(state.projectDir, "src", "config", "schemas", dataSourceName); - if (fs.existsSync(schemaFolderPath)) { - fs.removeSync(schemaFolderPath); - if (state.debug) { - p.note(`Removed schema folder at ${schemaFolderPath}`); - } - } - - // Run typegen to regenerate types - await runCodegenCommand(); - if (state.debug) { - p.note("Successfully regenerated types"); - } - } - - p.note(`Successfully removed data source "${dataSourceName}"`); -}; - -export const makeRemoveDataSourceCommand = () => { - const removeDataSourceCommand = new Command("data") - .description("Remove a data source from your project") - .option("--name ", "Name of the data source to remove") - .addOption(nonInteractiveOption) - .addOption(debugOption) - .action(async (options) => { - const schema = z.object({ - name: z.string().optional(), - }); - const validated = schema.parse(options); - await runRemoveDataSourceCommand(validated.name); - }); - - removeDataSourceCommand.hook("preAction", (_thisCommand, actionCommand) => { - initProgramState(actionCommand.opts()); - state.baseCommand = "remove"; - ensureProofKitProject({ commandName: "remove" }); - }); - - return removeDataSourceCommand; -}; diff --git a/packages/cli/src/cli/remove/index.ts b/packages/cli/src/cli/remove/index.ts deleted file mode 100644 index a825e6c5..00000000 --- a/packages/cli/src/cli/remove/index.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Command } from "commander"; -import * as p from "~/cli/prompts.js"; - -import { debugOption } from "~/globalOptions.js"; -import { initProgramState, state } from "~/state.js"; -import { getSettings } from "~/utils/parseSettings.js"; -import { abortIfCancel, ensureProofKitProject } from "../utils.js"; -import { makeRemoveDataSourceCommand, runRemoveDataSourceCommand } from "./data-source.js"; -import { makeRemovePageCommand, runRemovePageAction } from "./page.js"; -import { makeRemoveSchemaCommand, runRemoveSchemaAction } from "./schema.js"; - -export const runRemove = async (_name: string | undefined) => { - const settings = getSettings(); - - const removeType = abortIfCancel( - await p.select({ - message: "What do you want to remove from your project?", - options: [ - { label: "Page", value: "page" }, - { - label: "Schema", - value: "schema", - hint: "remove a table or layout schema", - }, - ...(settings.appType === "browser" - ? [ - { - label: "Data Source", - value: "data", - hint: "remove a database or FileMaker connection", - }, - ] - : []), - ], - }), - ); - - if (removeType === "data") { - await runRemoveDataSourceCommand(); - } else if (removeType === "page") { - await runRemovePageAction(); - } else if (removeType === "schema") { - await runRemoveSchemaAction(); - } -}; - -export function makeRemoveCommand() { - const removeCommand = new Command("remove") - .description("Remove a component from your project") - .argument("[name]", "Type of component to remove") - .addOption(debugOption) - .action(runRemove); - - removeCommand.hook("preAction", (_thisCommand, _actionCommand) => { - initProgramState(_actionCommand.opts()); - state.baseCommand = "remove"; - ensureProofKitProject({ commandName: "remove" }); - }); - removeCommand.hook("preSubcommand", (_thisCommand, _subCommand) => { - initProgramState(_subCommand.opts()); - state.baseCommand = "remove"; - ensureProofKitProject({ commandName: "remove" }); - }); - - // Add subcommands - removeCommand.addCommand(makeRemoveDataSourceCommand()); - removeCommand.addCommand(makeRemovePageCommand()); - removeCommand.addCommand(makeRemoveSchemaCommand()); - - return removeCommand; -} diff --git a/packages/cli/src/cli/remove/page.ts b/packages/cli/src/cli/remove/page.ts deleted file mode 100644 index 23137ee3..00000000 --- a/packages/cli/src/cli/remove/page.ts +++ /dev/null @@ -1,218 +0,0 @@ -import path from "node:path"; -import { Command } from "commander"; -import fs from "fs-extra"; -import { Node, type Project, type PropertyAssignment, SyntaxKind } from "ts-morph"; -import * as p from "~/cli/prompts.js"; - -import { debugOption, nonInteractiveOption } from "~/globalOptions.js"; -import { initProgramState, state } from "~/state.js"; -import { getSettings } from "~/utils/parseSettings.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; -import { abortIfCancel, ensureProofKitProject } from "../utils.js"; - -const getExistingRoutes = (project: Project): { label: string; href: string }[] => { - const navFilePath = path.join(state.projectDir, "src/app/navigation.tsx"); - - // If navigation file doesn't exist (e.g., webviewer apps), there are no nav routes to remove - if (!fs.existsSync(navFilePath)) { - return []; - } - - const sourceFile = project.addSourceFileAtPath(navFilePath); - - const routes: { label: string; href: string }[] = []; - - // Get primary routes - const primaryRoutes = sourceFile - .getVariableDeclaration("primaryRoutes") - ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression) - ?.getElements(); - - if (primaryRoutes) { - for (const element of primaryRoutes) { - if (Node.isObjectLiteralExpression(element)) { - const labelProp = element - .getProperties() - .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "label"); - const hrefProp = element - .getProperties() - .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "href"); - - const label = labelProp?.getInitializer()?.getText().replace(/['"]/g, ""); - const href = hrefProp?.getInitializer()?.getText().replace(/['"]/g, ""); - - if (label && href) { - routes.push({ label, href }); - } - } - } - } - - // Get secondary routes - const secondaryRoutes = sourceFile - .getVariableDeclaration("secondaryRoutes") - ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression) - ?.getElements(); - - if (secondaryRoutes) { - for (const element of secondaryRoutes) { - if (Node.isObjectLiteralExpression(element)) { - const labelProp = element - .getProperties() - .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "label"); - const hrefProp = element - .getProperties() - .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "href"); - - const label = labelProp?.getInitializer()?.getText().replace(/['"]/g, ""); - const href = hrefProp?.getInitializer()?.getText().replace(/['"]/g, ""); - - if (label && href) { - routes.push({ label, href }); - } - } - } - } - - return routes; -}; - -const removeRouteFromNav = async (project: Project, routeToRemove: string) => { - const navFilePath = path.join(state.projectDir, "src/app/navigation.tsx"); - - // Skip if there is no navigation file - if (!fs.existsSync(navFilePath)) { - return; - } - - const sourceFile = project.addSourceFileAtPath(navFilePath); - - // Remove from primary routes - const primaryRoutes = sourceFile - .getVariableDeclaration("primaryRoutes") - ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression); - - if (primaryRoutes) { - const elements = primaryRoutes.getElements(); - for (let i = elements.length - 1; i >= 0; i--) { - const element = elements[i]; - if (Node.isObjectLiteralExpression(element)) { - const hrefProp = element - .getProperties() - .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "href"); - - const href = hrefProp?.getInitializer()?.getText().replace(/['"]/g, ""); - - if (href === routeToRemove) { - primaryRoutes.removeElement(i); - } - } - } - } - - // Remove from secondary routes - const secondaryRoutes = sourceFile - .getVariableDeclaration("secondaryRoutes") - ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression); - - if (secondaryRoutes) { - const elements = secondaryRoutes.getElements(); - for (let i = elements.length - 1; i >= 0; i--) { - const element = elements[i]; - if (Node.isObjectLiteralExpression(element)) { - const hrefProp = element - .getProperties() - .find((prop): prop is PropertyAssignment => Node.isPropertyAssignment(prop) && prop.getName() === "href"); - - const href = hrefProp?.getInitializer()?.getText().replace(/['"]/g, ""); - - if (href === routeToRemove) { - secondaryRoutes.removeElement(i); - } - } - } - } - - await formatAndSaveSourceFiles(project); -}; - -export const runRemovePageAction = async (routeName?: string) => { - const _settings = getSettings(); - const projectDir = state.projectDir; - const project = getNewProject(projectDir); - - // Get existing routes - const routes = getExistingRoutes(project); - - if (routes.length === 0) { - return p.cancel("No pages found in the navigation."); - } - - let selectedRouteName = routeName; - if (!selectedRouteName) { - if (state.nonInteractive) { - throw new Error("Route is required in non-interactive mode."); - } - - selectedRouteName = abortIfCancel( - await p.select({ - message: "Select the page to remove", - options: routes.map((route) => ({ - label: `${route.label} (${route.href})`, - value: route.href, - })), - }), - ); - } - - if (!selectedRouteName.startsWith("/")) { - selectedRouteName = `/${selectedRouteName}`; - } - - const pagePath = - state.appType === "browser" - ? path.join(projectDir, "src/app/(main)", selectedRouteName) - : path.join(projectDir, "src/routes", selectedRouteName); - - const spinner = p.spinner(); - spinner.start("Removing page"); - - try { - // Check if directory exists - if (!fs.existsSync(pagePath)) { - spinner.stop("Page not found!"); - return p.cancel(`Page at ${selectedRouteName} does not exist`); - } - - // Remove from navigation first (if present) - await removeRouteFromNav(project, selectedRouteName); - - // Remove the page directory - await fs.remove(pagePath); - - spinner.stop("Page removed successfully!"); - } catch (error) { - spinner.stop("Failed to remove page!"); - console.error("Error removing page:", error); - process.exit(1); - } -}; - -export const makeRemovePageCommand = () => { - const removePageCommand = new Command("page") - .description("Remove a page from your project") - .argument("[route]", "The route of the page to remove") - .addOption(nonInteractiveOption) - .addOption(debugOption) - .action(async (route: string) => { - await runRemovePageAction(route); - }); - - removePageCommand.hook("preAction", (_thisCommand, actionCommand) => { - initProgramState(actionCommand.opts()); - state.baseCommand = "remove"; - ensureProofKitProject({ commandName: "remove" }); - }); - - return removePageCommand; -}; diff --git a/packages/cli/src/cli/remove/schema.ts b/packages/cli/src/cli/remove/schema.ts deleted file mode 100644 index 4cc40088..00000000 --- a/packages/cli/src/cli/remove/schema.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Command } from "commander"; -import { z } from "zod/v4"; -import * as p from "~/cli/prompts.js"; - -import { getExistingSchemas, removeLayout } from "~/generators/fmdapi.js"; -import { state } from "~/state.js"; -import { getSettings, type Settings } from "~/utils/parseSettings.js"; -import { abortIfCancel } from "../utils.js"; - -export const runRemoveSchemaAction = async (opts?: { - projectDir?: string; - settings?: Settings; - sourceName?: string; - schemaName?: string; -}) => { - const settings = opts?.settings ?? getSettings(); - const projectDir = opts?.projectDir ?? state.projectDir; - let sourceName = opts?.sourceName; - - // If there is more than one fm data source, prompt for which one to remove from - if (!sourceName && settings.dataSources.filter((s) => s.type === "fm").length > 1) { - const dataSourceName = await p.select({ - message: "Which FileMaker data source do you want to remove a layout from?", - options: settings.dataSources.filter((s) => s.type === "fm").map((s) => ({ label: s.name, value: s.name })), - }); - if (p.isCancel(dataSourceName)) { - p.cancel(); - process.exit(0); - } - sourceName = z.string().parse(dataSourceName); - } - - if (!sourceName) { - sourceName = "filemaker"; - } - - const dataSource = settings.dataSources.filter((s) => s.type === "fm").find((s) => s.name === sourceName); - if (!dataSource) { - throw new Error(`FileMaker data source ${sourceName} not found in your ProofKit config`); - } - - // Get existing schemas for this data source - const existingSchemas = getExistingSchemas({ - projectDir, - dataSourceName: sourceName, - }); - - if (existingSchemas.length === 0) { - p.note(`No layouts found in data source "${sourceName}"`, "Nothing to remove"); - return; - } - - // Show existing schemas and let user pick one to remove - const schemaToRemove = - opts?.schemaName ?? - abortIfCancel( - await p.select({ - message: "Select a layout to remove", - options: existingSchemas - .map((schema) => ({ - label: `${schema.layout} (${schema.schemaName})`, - value: schema.schemaName ?? "", - })) - .filter((opt) => opt.value !== ""), - }), - ); - - // Confirm removal - const confirmRemoval = await p.confirm({ - message: `Are you sure you want to remove the layout "${schemaToRemove}"?`, - initialValue: false, - }); - - if (p.isCancel(confirmRemoval) || !confirmRemoval) { - p.cancel("Operation cancelled"); - process.exit(0); - } - - // Remove the schema - await removeLayout({ - projectDir, - dataSourceName: sourceName, - schemaName: schemaToRemove, - runCodegen: true, - }); - - p.outro(`Layout "${schemaToRemove}" has been removed from your project`); -}; - -export const makeRemoveSchemaCommand = () => { - const removeSchemaCommand = new Command("layout") - .alias("schema") - .description("Remove a layout from your fmschema file") - .action(async (opts: { settings: Settings }) => { - const settings = opts.settings; - await runRemoveSchemaAction({ settings }); - }); - - return removeSchemaCommand; -}; diff --git a/packages/cli/src/cli/tanstack-query.ts b/packages/cli/src/cli/tanstack-query.ts deleted file mode 100644 index fb29fac0..00000000 --- a/packages/cli/src/cli/tanstack-query.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Command } from "commander"; -import * as p from "~/cli/prompts.js"; - -import { injectTanstackQuery } from "~/generators/tanstack-query.js"; - -export const runAddTanstackQueryCommand = async () => { - const spinner = p.spinner(); - spinner.start("Adding Tanstack Query"); - await injectTanstackQuery(); - spinner.stop("Tanstack Query added"); -}; - -export const makeAddTanstackQueryCommand = () => { - const addTanstackQueryCommand = new Command("tanstack-query") - .description("Add Tanstack Query to your project") - .action(runAddTanstackQueryCommand); - - return addTanstackQueryCommand; -}; diff --git a/packages/cli/src/cli/typegen/index.ts b/packages/cli/src/cli/typegen/index.ts deleted file mode 100644 index 23a4f61c..00000000 --- a/packages/cli/src/cli/typegen/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Command } from "commander"; - -import { runCodegenCommand } from "~/generators/fmdapi.js"; -import type { Settings } from "~/utils/parseSettings.js"; -import { ensureProofKitProject } from "../utils.js"; - -export async function runTypegen(_opts: { settings: Settings }) { - await runCodegenCommand(); -} - -export const makeTypegenCommand = () => { - const typegenCommand = new Command("typegen").description("Generate types for your project").action(runTypegen); - - typegenCommand.hook("preAction", (_thisCommand, actionCommand) => { - const settings = ensureProofKitProject({ commandName: "typegen" }); - actionCommand.setOptionValue("settings", settings); - }); - - return typegenCommand; -}; diff --git a/packages/cli/src/cli/update/index.ts b/packages/cli/src/cli/update/index.ts deleted file mode 100644 index 93eca92b..00000000 --- a/packages/cli/src/cli/update/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import chalk from "chalk"; -import { Command } from "commander"; - -import { initProgramState, state } from "~/state.js"; -import { runAllAvailableUpgrades } from "~/upgrades/index.js"; -import { logger } from "~/utils/logger.js"; -import { ensureProofKitProject } from "../utils.js"; - -export const runUpgrade = async () => { - initProgramState({}); - state.baseCommand = "upgrade"; - ensureProofKitProject({ commandName: "upgrade" }); - - logger.info("\nUpgrading ProofKit components...\n"); - - try { - await runAllAvailableUpgrades(); - logger.info(chalk.green("โœ” Successfully upgraded components\n")); - } catch (error) { - logger.error("Failed to upgrade components:", error); - process.exit(1); - } -}; - -export const upgrade = new Command() - .name("upgrade") - .description("Upgrade ProofKit components in your project") - .action(runUpgrade); diff --git a/packages/cli/src/cli/update/makeUpgradeCommand.ts b/packages/cli/src/cli/update/makeUpgradeCommand.ts deleted file mode 100644 index c3378db0..00000000 --- a/packages/cli/src/cli/update/makeUpgradeCommand.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Command } from "commander"; - -import { initProgramState, state } from "~/state.js"; -import { ensureProofKitProject } from "../utils.js"; -import { runUpgrade } from "./index.js"; - -export const makeUpgradeCommand = () => { - const upgradeCommand = new Command("upgrade") - .description("Upgrade ProofKit components in your project") - .action(async (args) => { - initProgramState(args); - - await runUpgrade(); - }); - - upgradeCommand.hook("preAction", (_thisCommand, _actionCommand) => { - initProgramState(_actionCommand.opts()); - state.baseCommand = "upgrade"; - ensureProofKitProject({ commandName: "upgrade" }); - }); - - return upgradeCommand; -}; diff --git a/packages/cli/src/cli/utils.ts b/packages/cli/src/cli/utils.ts deleted file mode 100644 index d0d6a30d..00000000 --- a/packages/cli/src/cli/utils.ts +++ /dev/null @@ -1,49 +0,0 @@ -import path from "node:path"; -import chalk from "chalk"; -import fs from "fs-extra"; -import z, { ZodError } from "zod/v4"; - -import { cancel, isCancel } from "~/cli/prompts.js"; -import { npmName } from "~/consts.js"; -import { UserCancelledError } from "~/core/errors.js"; -import { getSettings } from "~/utils/parseSettings.js"; - -/** - * Runs before any add command is run. Checks if the user is in a ProofKit project and if the - * proofkit.json file is valid. - */ -export const ensureProofKitProject = ({ commandName }: { commandName: string }) => { - const settingsExists = fs.existsSync(path.join(process.cwd(), "proofkit.json")); - if (!settingsExists) { - console.log( - chalk.yellow( - `The "${commandName}" command requires an existing ProofKit project. -Please run " ${npmName} init" first, or try this command again when inside a ProofKit project.`, - ), - ); - process.exit(1); - } - - try { - return getSettings(); - } catch (error) { - console.log(chalk.red("Error parsing ProofKit settings file:")); - if (error instanceof ZodError) { - console.log(z.prettifyError(error)); - } else { - console.log(error); - } - - process.exit(1); - } -}; - -export function abortIfCancel(value: symbol | string): string; -export function abortIfCancel(value: symbol | T): T; -export function abortIfCancel(value: T | symbol): T { - if (isCancel(value)) { - cancel(); - throw new UserCancelledError({ message: "User aborted the operation" }); - } - return value; -} diff --git a/packages/cli/src/consts.ts b/packages/cli/src/consts.ts deleted file mode 100644 index a76eb3c0..00000000 --- a/packages/cli/src/consts.ts +++ /dev/null @@ -1,55 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __filename = fileURLToPath(import.meta.url); -const distPath = path.dirname(__filename); -export const PKG_ROOT = process.env.PROOFKIT_PKG_ROOT ?? path.join(distPath, "../"); - -export const DEFAULT_APP_NAME = "my-proofkit-app"; -export const NODE_RUNTIME_VERSION = "^22.12.0 || ^24.0.0"; -export const cliName = "proofkit"; -export const npmName = "@proofkit/cli"; -export const DOCS_URL = "https://proofkit.proof.sh"; - -export function getAgentInstructions() { - return ` - ## ProofKit Documentation - ProofKit is a set of packages and opinions designed to work really well with FileMaker. Use the ProofKit docs as the primary reference for this project: https://proofkit.proof.sh/llms.txt - -## Data Loading -Always use tanstack/react-query instead of useState and useEffect when loading data or calling FileMaker scripts with fmFetch. - `; -} - -// Registry URL is injected at build time via tsdown define. -declare const __REGISTRY_URL__: string; -export const DEFAULT_REGISTRY_URL = - typeof __REGISTRY_URL__ !== "undefined" && __REGISTRY_URL__ ? __REGISTRY_URL__ : "https://proofkit.proof.sh"; -const TITLE_ASCII = ` - _______ ___ ___ ____ _ _ -|_ __ \\ .' ..]|_ ||_ _| (_) / |_ - | |__) |_ .--. .--. .--. _| |_ | |_/ / __ \`| |-' - | ___/[ \`/'\`\\]/ .'\`\\ \\/ .'\`\\ \\'-| |-' | __'. [ | | | - _| |_ | | | \\__. || \\__. | | | _| | \\ \\_ | | | |, -|_____| [___] '.__.' '.__.' [___] |____||____|[___]\\__/ -`; -export function getTitleText(version: string) { - const versionText = `v${version}`; - const lineWidth = 61; - const padding = Math.max(lineWidth - versionText.length, 0); - return `${TITLE_ASCII}${" ".repeat(padding)}${versionText}\n`; -} -function resolveTemplateRoot(): string { - const candidates = [path.join(PKG_ROOT, "template"), path.resolve(PKG_ROOT, "../cli/template")] as const; - - for (const candidate of candidates) { - if (fs.existsSync(candidate)) { - return candidate; - } - } - - return candidates[0]; -} - -export const TEMPLATE_ROOT = resolveTemplateRoot(); diff --git a/packages/cli/src/core/context.ts b/packages/cli/src/core/context.ts deleted file mode 100644 index 43572dd1..00000000 --- a/packages/cli/src/core/context.ts +++ /dev/null @@ -1,241 +0,0 @@ -import type { Effect as Fx } from "effect"; -import { Context } from "effect"; -import type { CliError } from "~/core/errors.js"; -import type { AppType, FileMakerEnvNames, FileMakerInputs, ProofKitSettings, UIType } from "~/core/types.js"; -import type { PackageManager } from "~/utils/packageManager.js"; - -type Eff = Fx.Effect; - -export interface CliContextValue { - cwd: string; - debug: boolean; - nonInteractive: boolean; - packageManager: PackageManager; - resolvedProjectConfig?: { - appType?: AppType; - ui?: UIType; - projectDir?: string; - }; -} - -export const CliContext = Context.GenericTag("@proofkit/cli/CliContext"); - -export interface PromptService { - readonly text: (options: { - message: string; - defaultValue?: string; - validate?: (value: string) => string | undefined; - }) => Promise; - readonly password: (options: { - message: string; - validate?: (value: string) => string | undefined; - }) => Promise; - readonly select: (options: { - message: string; - options: Array<{ - value: T; - label: string; - hint?: string; - disabled?: boolean | string; - }>; - }) => Promise; - readonly searchSelect: (options: { - message: string; - emptyMessage?: string; - options: Array<{ - value: T; - label: string; - hint?: string; - keywords?: string[]; - disabled?: boolean | string; - }>; - }) => Promise; - readonly multiSearchSelect: (options: { - message: string; - options: Array<{ - value: T; - label: string; - hint?: string; - keywords?: string[]; - disabled?: boolean | string; - }>; - required?: boolean; - }) => Promise; - readonly confirm: (options: { message: string; initialValue?: boolean }) => Promise; -} - -export const PromptService = Context.GenericTag("@proofkit/cli/PromptService"); - -export interface ConsoleService { - readonly info: (message: string) => void; - readonly warn: (message: string) => void; - readonly error: (message: string) => void; - readonly success: (message: string) => void; - readonly note: (message: string, title?: string) => void; -} - -export const ConsoleService = Context.GenericTag("@proofkit/cli/ConsoleService"); - -export interface FileSystemService { - readonly exists: (path: string) => Eff; - readonly readdir: (path: string) => Eff; - readonly ensureDir: (path: string) => Eff; - readonly emptyDir: (path: string) => Eff; - readonly copyDir: (from: string, to: string, options?: { overwrite?: boolean }) => Eff; - readonly rename: (from: string, to: string) => Eff; - readonly remove: (path: string) => Eff; - readonly readJson: (path: string) => Eff; - readonly writeJson: (path: string, value: unknown) => Eff; - readonly writeFile: (path: string, content: string) => Eff; - readonly readFile: (path: string) => Eff; -} - -export const FileSystemService = Context.GenericTag("@proofkit/cli/FileSystemService"); - -export interface TemplateService { - readonly getTemplateDir: (appType: AppType, ui: UIType) => string; -} - -export const TemplateService = Context.GenericTag("@proofkit/cli/TemplateService"); - -export interface PackageManagerService { - readonly getVersion: (packageManager: PackageManager, cwd: string) => Eff; -} - -export const PackageManagerService = Context.GenericTag("@proofkit/cli/PackageManagerService"); - -export interface ProcessService { - readonly run: ( - command: string, - args: string[], - options: { - cwd: string; - stdout?: "pipe" | "inherit" | "ignore"; - stderr?: "pipe" | "inherit" | "ignore"; - }, - ) => Eff<{ stdout: string; stderr: string }, CliError>; -} - -export const ProcessService = Context.GenericTag("@proofkit/cli/ProcessService"); - -export interface GitService { - readonly initialize: (projectDir: string) => Eff; -} - -export const GitService = Context.GenericTag("@proofkit/cli/GitService"); - -export interface SettingsService { - readonly writeSettings: (projectDir: string, settings: ProofKitSettings) => Eff; - readonly appendEnvVars: (projectDir: string, vars: Record) => Eff; -} - -export const SettingsService = Context.GenericTag("@proofkit/cli/SettingsService"); - -export interface FmMcpStatus { - baseUrl: string; - healthy: boolean; - connectedFiles: string[]; -} - -export interface FileMakerServerVersions { - fmsVersion: string; - ottoVersion: string | null; -} - -export interface OttoFileInfo { - filename: string; - status: string; -} - -export interface OttoApiKeyInfo { - key: string; - user: string; - database: string; - label: string; -} - -export interface FileMakerDataSourceEntry { - type: "fm"; - name: string; - envNames: FileMakerEnvNames; -} - -export interface FileMakerBootstrapArtifacts { - settings: ProofKitSettings; - envVars: Record; - envSchemaEntries: Array<{ - name: string; - zodSchema: string; - defaultValue: string; - }>; - typegenConfig: { - mode: FileMakerInputs["mode"]; - dataSourceName: string; - envNames?: FileMakerEnvNames; - fmMcpBaseUrl?: string; - connectedFileName?: string; - layoutName?: string; - schemaName?: string; - appType: AppType; - }; -} - -export interface FileMakerService { - readonly detectLocalFmMcp: (baseUrl?: string) => Eff; - readonly authorizeLocalFmMcp: (input: { - baseUrl: string; - fileName: string; - interactive: boolean; - clientName: string; - clientDescription: string; - }) => Eff<{ sessionToken: string }, CliError>; - readonly installLocalWebViewerAddon: () => Eff; - readonly validateHostedServerUrl: ( - serverUrl: string, - ottoPort?: number | null, - ) => Eff< - { - normalizedUrl: string; - versions: FileMakerServerVersions; - }, - CliError - >; - readonly getOttoFMSToken: (options: { url: URL }) => Eff<{ token: string }, CliError>; - readonly listFiles: (options: { url: URL; token: string }) => Eff; - readonly listAPIKeys: (options: { url: URL; token: string }) => Eff; - readonly createDataAPIKeyWithCredentials: (options: { - url: URL; - filename: string; - username: string; - password: string; - }) => Eff<{ apiKey: string }, CliError>; - readonly deployDemoFile: (options: { - url: URL; - token: string; - operation: "install" | "replace"; - }) => Eff<{ apiKey: string; filename: string }, CliError>; - readonly listLayouts: (options: { dataApiKey: string; fmFile: string; server: string }) => Eff; - readonly createFileMakerBootstrapArtifacts: ( - settings: ProofKitSettings, - inputs: FileMakerInputs, - appType: AppType, - ) => Eff; - readonly bootstrap: ( - projectDir: string, - settings: ProofKitSettings, - inputs: FileMakerInputs, - appType: AppType, - ) => Eff; -} - -export const FileMakerService = Context.GenericTag("@proofkit/cli/FileMakerService"); - -export interface CodegenService { - readonly runInitial: ( - projectDir: string, - packageManager: PackageManager, - proofkitToken?: string, - ) => Eff; -} - -export const CodegenService = Context.GenericTag("@proofkit/cli/CodegenService"); diff --git a/packages/cli/src/core/doctor.ts b/packages/cli/src/core/doctor.ts deleted file mode 100644 index 9ce75dff..00000000 --- a/packages/cli/src/core/doctor.ts +++ /dev/null @@ -1,320 +0,0 @@ -import path from "node:path"; -import { parse as parseDotenv } from "dotenv"; -import { Effect } from "effect"; -import { parse as parseJsonc } from "jsonc-parser/lib/esm/main.js"; -import { DOCS_URL } from "~/consts.js"; -import { CliContext, ConsoleService, FileSystemService } from "~/core/context.js"; - -interface TypegenConfigEntry { - type?: string; - path?: string; - fmMcp?: { enabled?: boolean; connectedFileName?: string }; - layouts?: unknown[]; - tables?: unknown[]; - envNames?: { - server?: string; - db?: string; - auth?: { - apiKey?: string; - username?: string; - password?: string; - }; - }; -} - -function pushUnique(target: string[], value: string | undefined) { - if (value && !target.includes(value)) { - target.push(value); - } -} - -function isTypegenConfigLike(value: unknown): value is { - config: TypegenConfigEntry | TypegenConfigEntry[]; -} { - return value !== null && typeof value === "object" && "config" in value; -} - -export const runDoctor = Effect.gen(function* () { - const cliContext = yield* CliContext; - const fs = yield* FileSystemService; - const consoleService = yield* ConsoleService; - const cwd = cliContext.cwd; - const readJsonSafe = (targetPath: string) => - fs.readJson(targetPath).pipe( - Effect.match({ - onFailure: () => undefined, - onSuccess: (value) => value, - }), - ); - const readFileSafe = (targetPath: string) => - fs.readFile(targetPath).pipe( - Effect.match({ - onFailure: () => undefined, - onSuccess: (value) => value, - }), - ); - - const settingsPath = path.join(cwd, "proofkit.json"); - if (!(yield* fs.exists(settingsPath))) { - consoleService.note( - [ - "No ProofKit project found in this directory.", - "", - "Next steps:", - "- Run `proofkit init` to create a new project", - `- Docs: ${DOCS_URL}/docs/cli`, - ].join("\n"), - "Doctor", - ); - return; - } - - const findings: { level: "ok" | "warn" | "error"; message: string }[] = []; - - let settings: - | { - appType?: string; - envFile?: string; - dataSources?: { - type?: string; - envNames?: { - database?: string; - server?: string; - apiKey?: string; - }; - }[]; - } - | undefined; - - settings = yield* readJsonSafe(settingsPath); - if (settings) { - findings.push({ level: "ok", message: "Found `proofkit.json`." }); - } else { - findings.push({ - level: "error", - message: "Could not read `proofkit.json`.", - }); - } - - const packageJsonPath = path.join(cwd, "package.json"); - let packageJson: - | { - scripts?: Record; - dependencies?: Record; - devDependencies?: Record; - } - | undefined; - - if (yield* fs.exists(packageJsonPath)) { - const nextPackageJson = yield* readJsonSafe>(packageJsonPath); - if (nextPackageJson) { - packageJson = nextPackageJson; - const allDeps = { - ...(nextPackageJson.dependencies ?? {}), - ...(nextPackageJson.devDependencies ?? {}), - }; - - if (allDeps["@proofkit/typegen"]) { - findings.push({ level: "ok", message: "Found `@proofkit/typegen`." }); - } else { - findings.push({ - level: "warn", - message: "Missing `@proofkit/typegen` dependency.", - }); - } - - if (nextPackageJson.scripts?.typegen) { - findings.push({ level: "ok", message: "Found `typegen` script." }); - } else { - findings.push({ - level: "warn", - message: "Missing `typegen` script in `package.json`.", - }); - } - - if (nextPackageJson.scripts?.["typegen:ui"]) { - findings.push({ level: "ok", message: "Found `typegen:ui` script." }); - } - } else { - findings.push({ - level: "error", - message: "Could not read `package.json`.", - }); - } - } else { - findings.push({ level: "error", message: "Missing `package.json`." }); - } - - const typegenConfigPath = path.join(cwd, "proofkit-typegen.config.jsonc"); - let parsedTypegenConfig: - | { - config: TypegenConfigEntry | TypegenConfigEntry[]; - } - | undefined; - - if (yield* fs.exists(typegenConfigPath)) { - const raw = yield* readFileSafe(typegenConfigPath); - if (raw) { - const parsed = parseJsonc(raw); - if (isTypegenConfigLike(parsed)) { - parsedTypegenConfig = parsed; - findings.push({ - level: "ok", - message: "Typegen config is present and valid.", - }); - - const configEntries = Array.isArray(parsed.config) ? parsed.config : [parsed.config]; - for (const entry of configEntries) { - const outputPath = path.join(cwd, entry.path ?? "schema"); - if (yield* fs.exists(outputPath)) { - findings.push({ - level: "ok", - message: `Generated path exists: \`${entry.path ?? "schema"}\`.`, - }); - } else { - findings.push({ - level: "warn", - message: `Generated path missing: \`${entry.path ?? "schema"}\`. Run \`npx @proofkit/typegen\`.`, - }); - } - - if (entry.type === "fmdapi" && (entry.layouts?.length ?? 0) === 0) { - findings.push({ - level: "warn", - message: "Typegen config has no layouts yet. Use `npx @proofkit/typegen ui`.", - }); - } - - if (entry.type === "fmodata" && (entry.tables?.length ?? 0) === 0) { - findings.push({ - level: "warn", - message: "Typegen config has no tables yet. Use `npx @proofkit/typegen ui`.", - }); - } - - if (entry.type === "fmdapi" && entry.fmMcp?.enabled && !entry.fmMcp.connectedFileName) { - findings.push({ - level: "warn", - message: "FM MCP is enabled but no connected file is pinned yet.", - }); - } - } - } else { - findings.push({ - level: "error", - message: "Typegen config exists but is invalid. Open `npx @proofkit/typegen ui` or fix the JSONC file.", - }); - } - } else { - findings.push({ - level: "error", - message: "Could not read `proofkit-typegen.config.jsonc`.", - }); - } - } else { - findings.push({ - level: "warn", - message: "Missing `proofkit-typegen.config.jsonc`. Run `npx @proofkit/typegen init`.", - }); - } - - const envCandidates = [ - settings?.envFile ? path.join(cwd, settings.envFile) : undefined, - path.join(cwd, ".env.local"), - path.join(cwd, ".env"), - ].filter((value): value is string => Boolean(value)); - - let resolvedEnvPath: string | undefined; - for (const candidate of envCandidates) { - if (yield* fs.exists(candidate)) { - resolvedEnvPath = candidate; - break; - } - } - - const expectedEnvNames: string[] = []; - for (const source of settings?.dataSources ?? []) { - if (source.type !== "fm") { - continue; - } - pushUnique(expectedEnvNames, source.envNames?.server); - pushUnique(expectedEnvNames, source.envNames?.database); - pushUnique(expectedEnvNames, source.envNames?.apiKey); - } - - let configEntries: TypegenConfigEntry[] = []; - if (parsedTypegenConfig) { - configEntries = Array.isArray(parsedTypegenConfig.config) - ? parsedTypegenConfig.config - : [parsedTypegenConfig.config]; - } - - for (const entry of configEntries) { - pushUnique(expectedEnvNames, entry.envNames?.server); - pushUnique(expectedEnvNames, entry.envNames?.db); - pushUnique(expectedEnvNames, entry.envNames?.auth?.apiKey); - pushUnique(expectedEnvNames, entry.envNames?.auth?.username); - pushUnique(expectedEnvNames, entry.envNames?.auth?.password); - } - - if (expectedEnvNames.length > 0) { - if (resolvedEnvPath) { - const envRaw = yield* readFileSafe(resolvedEnvPath); - if (envRaw) { - const env = parseDotenv(envRaw); - const missing = expectedEnvNames.filter((name) => !(name in env)); - if (missing.length > 0) { - findings.push({ - level: "warn", - message: `Missing env vars in \`${path.basename(resolvedEnvPath)}\`: ${missing.join(", ")}.`, - }); - } else { - findings.push({ - level: "ok", - message: `Expected env vars found in \`${path.basename(resolvedEnvPath)}\`.`, - }); - } - } else { - findings.push({ - level: "error", - message: `Could not read env file \`${path.basename(resolvedEnvPath)}\`.`, - }); - } - } else { - findings.push({ - level: "warn", - message: `No env file found. Expected vars: ${expectedEnvNames.join(", ")}.`, - }); - } - } - - const errors = findings.filter((finding) => finding.level === "error"); - const warnings = findings.filter((finding) => finding.level === "warn"); - const oks = findings.filter((finding) => finding.level === "ok"); - - const lines = [ - `Checks: ${oks.length} ok, ${warnings.length} warn, ${errors.length} error`, - "", - ...findings.map((finding) => { - let prefix = "ERR"; - if (finding.level === "ok") { - prefix = "OK"; - } else if (finding.level === "warn") { - prefix = "WARN"; - } - return `- [${prefix}] ${finding.message}`; - }), - "", - "Next steps:", - "- Run `npx @proofkit/typegen init` if typegen config is missing", - "- Run `npx @proofkit/typegen ui` to edit typegen config", - "- Run `npx @proofkit/typegen` to regenerate generated files", - `- Docs: ${DOCS_URL}/docs/typegen`, - ]; - - if (settings?.appType === "webviewer") { - lines.splice(lines.length - 1, 0, "- For webviewer projects, make sure local FM MCP is running before typegen"); - } - - consoleService.note(lines.join("\n"), "Doctor"); -}); diff --git a/packages/cli/src/core/errors.ts b/packages/cli/src/core/errors.ts deleted file mode 100644 index d5aff9f0..00000000 --- a/packages/cli/src/core/errors.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Data } from "effect"; - -export class CliValidationError extends Data.TaggedError("CliValidationError")<{ - readonly message: string; - readonly cause?: unknown; -}> {} - -export class UserCancelledError extends Data.TaggedError("UserCancelledError")<{ - readonly message: string; -}> {} - -export class NonInteractiveInputError extends Data.TaggedError("NonInteractiveInputError")<{ - readonly message: string; -}> {} - -export class DirectoryConflictError extends Data.TaggedError("DirectoryConflictError")<{ - readonly message: string; - readonly path: string; -}> {} - -export class FileMakerSetupError extends Data.TaggedError("FileMakerSetupError")<{ - readonly message: string; - readonly cause?: unknown; -}> {} - -export class RegistryError extends Data.TaggedError("RegistryError")<{ - readonly message: string; - readonly cause?: unknown; -}> {} - -export class ExternalCommandError extends Data.TaggedError("ExternalCommandError")<{ - readonly message: string; - readonly command: string; - readonly args: readonly string[]; - readonly cwd: string; - readonly cause?: unknown; -}> {} - -export class FileSystemError extends Data.TaggedError("FileSystemError")<{ - readonly message: string; - readonly operation: string; - readonly path: string; - readonly cause?: unknown; -}> {} - -export type CliError = - | CliValidationError - | UserCancelledError - | NonInteractiveInputError - | DirectoryConflictError - | FileMakerSetupError - | RegistryError - | ExternalCommandError - | FileSystemError; - -const cliErrorTags = new Set([ - "CliValidationError", - "UserCancelledError", - "NonInteractiveInputError", - "DirectoryConflictError", - "FileMakerSetupError", - "RegistryError", - "ExternalCommandError", - "FileSystemError", -]); - -export function isCliError(error: unknown): error is CliError { - return ( - typeof error === "object" && - error !== null && - "_tag" in error && - typeof error._tag === "string" && - "message" in error && - typeof error.message === "string" && - cliErrorTags.has(error._tag) - ); -} - -export function getCliErrorMessage(error: CliError) { - return error.message; -} diff --git a/packages/cli/src/core/executeInitPlan.ts b/packages/cli/src/core/executeInitPlan.ts deleted file mode 100644 index fee1390a..00000000 --- a/packages/cli/src/core/executeInitPlan.ts +++ /dev/null @@ -1,575 +0,0 @@ -import path from "node:path"; -import { Chalk } from "chalk"; -import { Cause, Effect, Exit } from "effect"; -import { getOrUndefined } from "effect/Option"; - -import { getAgentInstructions } from "~/consts.js"; -import { - CliContext, - CodegenService, - ConsoleService, - FileMakerService, - FileSystemService, - GitService, - PackageManagerService, - ProcessService, - PromptService, - SettingsService, -} from "~/core/context.js"; -import { - type CliError, - DirectoryConflictError, - type ExternalCommandError, - FileSystemError, - isCliError, - UserCancelledError, -} from "~/core/errors.js"; -import { applyPackageJsonMutations } from "~/core/planInit.js"; -import type { InitPlan } from "~/core/types.js"; -import { getIntentInstallCommand } from "~/helpers/intent.js"; -import { - getBrowserOxlintConfig, - getHuskyPreCommitHook, - getUltraciteInitCommand, - getWebViewerOxlintConfig, -} from "~/helpers/ultracite.js"; -import { - formatPackageManagerCommand, - normalizeImportAlias, - parseCommandString, - replaceTextInFiles, - updateTypegenConfig, -} from "~/utils/projectFiles.js"; -import { isCancel } from "~/utils/prompts.js"; -import { sortPackageJson } from "~/utils/sortPackageJson.js"; - -const AGENT_METADATA_DIRS = new Set([".agents", ".claude", ".clawed", ".clinerules", ".cursor", ".windsurf"]); -const IMPORT_ALIAS_WILDCARD_REGEX = /\*/g; -const IMPORT_ALIAS_TRAILING_SLASH_REGEX = /\/?$/; -const chalk = new Chalk({ level: 1 }); - -const formatCommand = (command: string) => chalk.cyan(command); -const formatHeading = (heading: string) => chalk.bold(heading); -const formatPath = (value: string) => chalk.yellow(value); -const NPM_PACKAGE_MANAGER_WARNING = - "Warning: We strongly suggest using PNPM 11 or greater as your package manager to better protect your computer and your app."; - -function getCauseText(cause: unknown) { - if (typeof cause !== "object" || cause === null) { - return cause ? String(cause) : undefined; - } - - const details = cause as { - shortMessage?: unknown; - message?: unknown; - stderr?: unknown; - stdout?: unknown; - }; - - return [details.shortMessage, details.stderr, details.stdout, details.message] - .filter((value): value is string => typeof value === "string") - .map((value) => value.trim()) - .find((value) => value.length > 0); -} - -function formatExternalCommand(error: ExternalCommandError) { - return [error.command, ...error.args].join(" "); -} - -function isExternalCommandError(error: CliError): error is ExternalCommandError { - return error._tag === "ExternalCommandError"; -} - -function renderInstallFailure(plan: InitPlan, error: CliError) { - const failedCommand = isExternalCommandError(error) - ? formatExternalCommand(error) - : `${plan.request.packageManager} install`; - const lines = [ - chalk.red(formatHeading("Install failed.")), - `${formatHeading("Project root:")} ${formatPath(plan.targetDir)}`, - `${formatHeading("Failed command:")} ${formatCommand(failedCommand)}`, - `${formatHeading("Succeeded before failure:")} scaffold files, package.json, proofkit.json, env file, editor config files`, - ]; - const causeText = getCauseText("cause" in error ? error.cause : undefined); - if (causeText && causeText !== error.message) { - lines.push(`${formatHeading("Reason:")} ${causeText}`); - } else { - lines.push(`${formatHeading("Reason:")} ${error.message}`); - } - - lines.push( - "", - formatHeading("Continue troubleshooting:"), - ` ${formatCommand(`cd ${plan.request.appDir}`)}`, - ` ${formatCommand(failedCommand)}`, - "", - formatHeading("Start over:"), - ` Remove ${formatPath(plan.targetDir)}, then rerun the init command.`, - ); - - return lines.join("\n"); -} - -function renderNextSteps(plan: InitPlan, additionalSteps: string[] = []) { - const lines = [`${formatHeading("Project root:")} ${formatCommand(`cd ${formatPath(plan.request.appDir)}`)}`]; - - if (plan.request.noInstall) { - lines.push( - "", - formatHeading("Install dependencies:"), - ` ${formatCommand(plan.request.packageManager === "yarn" ? "yarn" : `${plan.request.packageManager} install`)}`, - ); - } - - if (plan.request.packageManager === "npm") { - lines.push("", chalk.yellow(NPM_PACKAGE_MANAGER_WARNING)); - } - - lines.push("", formatHeading("Start the app:"), ` ${formatCommand(`${plan.packageManagerCommand} dev`)}`); - - if (plan.request.appType === "webviewer") { - lines.push( - "", - formatHeading("When your FileMaker file is ready:"), - ` ${formatCommand(`${plan.packageManagerCommand} typegen`)}`, - ` ${formatCommand(`${plan.packageManagerCommand} launch-fm`)}`, - ); - - if (additionalSteps.length > 0) { - lines.push(...additionalSteps.map((step) => ` ${formatCommand(step)}`)); - } - } - - return lines.join("\n"); -} - -function getPackageScriptCommand(plan: InitPlan, scriptName: string) { - const [command, ...args] = parseCommandString(formatPackageManagerCommand(plan.request.packageManager, scriptName)); - if (!command) { - throw new Error(`Unable to resolve ${scriptName} command for ${plan.request.packageManager}.`); - } - return { command, args }; -} - -function getMeaningfulDirectoryEntries(entries: string[]) { - return entries.filter((entry) => { - if (AGENT_METADATA_DIRS.has(entry)) { - return false; - } - if (entry === ".gitignore") { - return true; - } - if (entry.startsWith(".")) { - return false; - } - return true; - }); -} - -function promptEffect(message: string, run: () => Promise, targetPath = "") { - return Effect.tryPromise({ - try: async () => { - const value = await run(); - if (isCancel(value)) { - throw new DirectoryConflictError({ - message, - path: targetPath, - }); - } - return value; - }, - catch: (cause) => - isCliError(cause) - ? cause - : new DirectoryConflictError({ - message, - path: targetPath, - }), - }); -} - -export const prepareDirectory = (plan: InitPlan) => - Effect.gen(function* () { - const fs = yield* FileSystemService; - const consoleService = yield* ConsoleService; - const cliContext = yield* CliContext; - const prompts = yield* PromptService; - - const exists = yield* fs.exists(plan.targetDir); - if (!exists) { - return; - } - - const entries = yield* fs.readdir(plan.targetDir); - const meaningfulEntries = getMeaningfulDirectoryEntries(entries); - if (meaningfulEntries.length === 0) { - return; - } - - if (plan.request.force) { - yield* fs.emptyDir(plan.targetDir); - return; - } - - if (cliContext.nonInteractive) { - return yield* Effect.fail( - new DirectoryConflictError({ - message: `${plan.request.appDir} already exists and isn't empty. Remove the existing files or choose a different directory.`, - path: plan.targetDir, - }), - ); - } - - const overwriteMode = yield* promptEffect( - "Unable to choose how to handle the existing directory.", - () => - prompts.select({ - message: `${plan.request.appDir} already exists and isn't empty. How would you like to proceed?`, - options: [ - { value: "abort", label: "Abort installation" }, - { value: "clear", label: "Clear the directory and continue" }, - { - value: "overwrite", - label: "Continue and overwrite conflicting files", - }, - ], - }), - plan.targetDir, - ); - - if (overwriteMode === "abort") { - return yield* Effect.fail( - new UserCancelledError({ - message: "User aborted the operation", - }), - ); - } - - if (overwriteMode === "clear") { - const confirmed = yield* promptEffect( - "Unable to confirm directory clearing.", - () => - prompts.confirm({ - message: "Are you sure you want to clear the directory?", - initialValue: false, - }), - plan.targetDir, - ); - if (!confirmed) { - return yield* Effect.fail( - new UserCancelledError({ - message: "User aborted the operation", - }), - ); - } - yield* fs.emptyDir(plan.targetDir); - return; - } - - consoleService.warn(`Continuing in ${plan.request.appDir} and overwriting conflicting files when needed.`); - }); - -export const executeInitPlan = (plan: InitPlan) => - Effect.gen(function* () { - const cliContext = yield* CliContext; - const fs = yield* FileSystemService; - const consoleService = yield* ConsoleService; - const settingsService = yield* SettingsService; - const fileMakerService = yield* FileMakerService; - const processService = yield* ProcessService; - const gitService = yield* GitService; - const codegenService = yield* CodegenService; - const packageManagerService = yield* PackageManagerService; - const additionalNextSteps: string[] = []; - const runFileSystemPromise = async (effect: Effect.Effect) => { - const exit = await Effect.runPromiseExit(effect); - if (Exit.isSuccess(exit)) { - return exit.value; - } - - const failure = getOrUndefined(Cause.failureOption(exit.cause)); - if (failure && typeof failure === "object" && failure !== null && "cause" in failure) { - throw failure.cause; - } - - throw failure ?? Cause.squash(exit.cause); - }; - const projectFilesFs = { - exists: (targetPath: string) => runFileSystemPromise(fs.exists(targetPath)), - readdir: (targetPath: string) => runFileSystemPromise(fs.readdir(targetPath)), - readFile: (targetPath: string) => runFileSystemPromise(fs.readFile(targetPath)), - writeFile: (targetPath: string, content: string) => runFileSystemPromise(fs.writeFile(targetPath, content)), - }; - - yield* prepareDirectory(plan); - - consoleService.info(`Scaffolding in ${plan.targetDir}`); - yield* fs.copyDir(plan.templateDir, plan.targetDir, { overwrite: true }); - - const stagedGitignore = path.join(plan.targetDir, "_gitignore"); - const finalGitignore = path.join(plan.targetDir, ".gitignore"); - if (yield* fs.exists(stagedGitignore)) { - if (yield* fs.exists(finalGitignore)) { - yield* fs.remove(stagedGitignore); - } else { - yield* fs.rename(stagedGitignore, finalGitignore); - } - } - - const packageJsonPath = path.join(plan.targetDir, "package.json"); - const packageJson = yield* fs.readJson>(packageJsonPath); - const updatedPackageJson = sortPackageJson( - applyPackageJsonMutations(packageJson as never, plan.packageJson) as never, - ); - yield* fs.writeJson(packageJsonPath, updatedPackageJson); - - yield* settingsService.writeSettings(plan.targetDir, plan.settings); - yield* fs.writeFile(plan.envFile.path, plan.envFile.content); - for (const write of plan.writes) { - yield* fs.writeFile(write.path, write.content); - } - - yield* Effect.tryPromise({ - try: () => replaceTextInFiles(projectFilesFs, plan.targetDir, "__PNPM_COMMAND__", plan.packageManagerCommand), - catch: (cause) => - new FileSystemError({ - message: "Unable to rewrite scaffold placeholders.", - operation: "replaceTextInFiles", - path: plan.targetDir, - cause, - }), - }); - yield* Effect.tryPromise({ - try: () => - replaceTextInFiles( - projectFilesFs, - plan.targetDir, - "__PNPM_EXECUTE_COMMAND__", - plan.packageManagerExecuteCommand, - ), - catch: (cause) => - new FileSystemError({ - message: "Unable to rewrite scaffold placeholders.", - operation: "replaceTextInFiles", - path: plan.targetDir, - cause, - }), - }); - yield* Effect.tryPromise({ - try: () => replaceTextInFiles(projectFilesFs, plan.targetDir, "__PACKAGE_MANAGER__", plan.request.packageManager), - catch: (cause) => - new FileSystemError({ - message: "Unable to rewrite scaffold placeholders.", - operation: "replaceTextInFiles", - path: plan.targetDir, - cause, - }), - }); - yield* Effect.tryPromise({ - try: () => replaceTextInFiles(projectFilesFs, plan.targetDir, "__AGENT_INSTRUCTIONS__", getAgentInstructions()), - catch: (cause) => - new FileSystemError({ - message: "Unable to rewrite scaffold placeholders.", - operation: "replaceTextInFiles", - path: plan.targetDir, - cause, - }), - }); - if (plan.request.importAlias !== "~/") { - yield* Effect.tryPromise({ - try: () => - replaceTextInFiles(projectFilesFs, plan.targetDir, "~/", normalizeImportAlias(plan.request.importAlias)), - catch: (cause) => - new FileSystemError({ - message: "Unable to rewrite scaffold import aliases.", - operation: "replaceTextInFiles", - path: plan.targetDir, - cause, - }), - }); - yield* Effect.tryPromise({ - try: () => - replaceTextInFiles( - projectFilesFs, - plan.targetDir, - "@/", - plan.request.importAlias - .replace(IMPORT_ALIAS_WILDCARD_REGEX, "") - .replace(IMPORT_ALIAS_TRAILING_SLASH_REGEX, "/"), - ), - catch: (cause) => - new FileSystemError({ - message: "Unable to rewrite scaffold import aliases.", - operation: "replaceTextInFiles", - path: plan.targetDir, - cause, - }), - }); - } - - let nextSettings = plan.settings; - if (plan.tasks.bootstrapFileMaker && plan.request.fileMaker) { - const fileMakerInputs = plan.request.fileMaker; - nextSettings = yield* fileMakerService.bootstrap( - plan.targetDir, - nextSettings, - fileMakerInputs, - plan.request.appType, - ); - yield* settingsService.writeSettings(plan.targetDir, nextSettings); - } - - if (plan.request.appType === "webviewer" && !plan.tasks.bootstrapFileMaker) { - const localFmMcp = yield* fileMakerService.detectLocalFmMcp(); - const connectedFiles = localFmMcp.connectedFiles.filter(Boolean); - if (localFmMcp.healthy && connectedFiles.length === 1) { - const detectedFile = connectedFiles[0]; - if (detectedFile) { - yield* Effect.tryPromise({ - try: () => - updateTypegenConfig(projectFilesFs, plan.targetDir, { - appType: "webviewer", - dataSourceName: "filemaker", - fmMcpBaseUrl: localFmMcp.baseUrl, - connectedFileName: detectedFile, - }), - catch: (cause) => - new FileSystemError({ - message: "Unable to persist local FileMaker file detection into typegen config.", - operation: "updateTypegenConfig", - path: plan.targetDir, - cause, - }), - }); - } - } - } - - if (plan.tasks.checkWebViewerAddon) { - yield* Effect.promise(async () => { - try { - const { checkForWebViewerLayouts, getWebViewerAddonMessages } = await import( - "~/installers/proofkit-webviewer.js" - ); - const status = await checkForWebViewerLayouts(plan.targetDir); - const messages = getWebViewerAddonMessages(status); - - for (const message of messages.warn) { - consoleService.warn(message); - } - for (const message of messages.info) { - consoleService.info(message); - } - if (cliContext.nonInteractive) { - additionalNextSteps.push(...messages.nextSteps); - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error); - consoleService.warn(`Could not inspect the ProofKit Web Viewer add-on (${message}).`); - } - }); - } - - if (plan.tasks.runInstall) { - let installArgs: string[] = ["install"]; - if (plan.request.packageManager === "yarn") { - installArgs = []; - } - const installResult = yield* Effect.either( - processService.run(plan.request.packageManager, installArgs, { - cwd: plan.targetDir, - stdout: "pipe", - stderr: "pipe", - }), - ); - if (installResult._tag === "Left") { - consoleService.error(renderInstallFailure(plan, installResult.left)); - return yield* Effect.fail(installResult.left); - } - } - - if (plan.tasks.runUltraciteInit) { - if (!plan.request.noInstall) { - const ultraciteCommand = getUltraciteInitCommand({ - appType: plan.request.appType, - packageManager: plan.request.packageManager, - skipInstall: plan.request.noInstall, - }); - yield* processService.run(ultraciteCommand.command, ultraciteCommand.args, { - cwd: plan.targetDir, - stdout: "pipe", - stderr: "pipe", - }); - } - - const oxlintConfigContent = - plan.request.appType === "browser" ? getBrowserOxlintConfig() : getWebViewerOxlintConfig(); - yield* fs.writeFile(path.join(plan.targetDir, "oxlint.config.ts"), oxlintConfigContent); - yield* fs.ensureDir(path.join(plan.targetDir, ".husky")); - yield* fs.writeFile(path.join(plan.targetDir, ".husky/pre-commit"), getHuskyPreCommitHook()); - } - - if (plan.tasks.runIntentInstall) { - const intentCommand = getIntentInstallCommand(plan.request.packageManager); - yield* processService.run(intentCommand.command, intentCommand.args, { - cwd: plan.targetDir, - stdout: "pipe", - stderr: "pipe", - }); - } - - if (plan.tasks.runInitialCodegen) { - yield* codegenService.runInitial(plan.targetDir, plan.request.packageManager, plan.request.proofkitToken); - } - - // plan.tasks.runFix is non-blocking: getPackageScriptCommand/processService.run can fail on fresh scaffolds. - // Effect.either also catches lint failures below and logs warnings; other errors still propagate. - if (plan.tasks.runFix) { - const fixCommand = getPackageScriptCommand(plan, "fix"); - yield* Effect.either( - processService.run(fixCommand.command, fixCommand.args, { - cwd: plan.targetDir, - stdout: "pipe", - stderr: "pipe", - }), - ); - } - - if (plan.tasks.runLint) { - const lintCommand = getPackageScriptCommand(plan, "lint"); - const result = yield* Effect.either( - processService.run(lintCommand.command, lintCommand.args, { - cwd: plan.targetDir, - stdout: "pipe", - stderr: "pipe", - }), - ); - if (result._tag === "Left") { - consoleService.warn("Lint did not succeed; continuing setup."); - } - } - - if (plan.tasks.initializeGit) { - yield* gitService.initialize(plan.targetDir); - } - - const packageManagerVersionResult = plan.request.noInstall - ? yield* Effect.either(packageManagerService.getVersion(plan.request.packageManager, plan.targetDir)) - : yield* packageManagerService.getVersion(plan.request.packageManager, plan.targetDir).pipe( - Effect.map((version) => ({ - _tag: "Right" as const, - right: version, - })), - ); - const packageManagerVersion = - packageManagerVersionResult._tag === "Right" ? packageManagerVersionResult.right : undefined; - - consoleService.success( - `Created ${plan.request.scopedAppName} in ${plan.targetDir}${ - packageManagerVersion ? ` using ${plan.request.packageManager}@${packageManagerVersion}` : "" - }`, - ); - consoleService.info(chalk.bold("Next steps:")); - consoleService.info(renderNextSteps(plan, Array.from(new Set(additionalNextSteps)))); - return plan; - }); diff --git a/packages/cli/src/core/planInit.ts b/packages/cli/src/core/planInit.ts deleted file mode 100644 index 78df8ca3..00000000 --- a/packages/cli/src/core/planInit.ts +++ /dev/null @@ -1,266 +0,0 @@ -import path from "node:path"; -import type { PackageJson } from "type-fest"; - -import { NODE_RUNTIME_VERSION } from "~/consts.js"; -import type { InitPlan, InitRequest, ProofKitSettings } from "~/core/types.js"; -import { - getFmdapiVersion, - getProofkitDependencyVersion, - getProofkitWebviewerVersion, - getTypegenVersion, -} from "~/utils/getProofKitVersion.js"; -import { - formatPackageManagerCommand, - getScaffoldVersion, - getTemplatePackageCommand, - getTemplatePackageExecuteCommand, -} from "~/utils/projectFiles.js"; -import { getNodeMajorVersion } from "~/utils/versioning.js"; - -const SHARED_PNPM_BUILD_POLICY = { - "@parcel/watcher": true, - esbuild: true, - "msgpackr-extract": true, - msw: true, - node: true, -} as const; -const NPM_PACKAGE_MANAGER_WARNING = - "Warning: We strongly suggest using PNPM 11 or greater as your package manager to better protect your computer and your app."; -const NPM_MIN_RELEASE_AGE_DAYS = 1; - -function createPackageManagerVersionRange(version: string) { - return version.startsWith("^") ? version : `^${version}`; -} - -export function createPnpmWorkspaceFileContent(appType: InitRequest["appType"]) { - const buildPolicy = { - ...SHARED_PNPM_BUILD_POLICY, - sharp: appType === "browser", - } as const; - - return [ - "# This setting defines where in the repo your apps/packages that need installed dependancies exist. This of this as a list of paths to your package.json files. ", - "packages:", - ' - "."', - "", - "allowBuilds:", - ...Object.entries(buildPolicy).map(([packageName, allowed]) => ` ${JSON.stringify(packageName)}: ${allowed}`), - "", - "trustPolicy: no-downgrade", - "", - "trustPolicyIgnoreAfter: 43200", - "", - "blockExoticSubdeps: true", - "", - ].join("\n"); -} - -export function createNpmrcFileContent() { - return [ - "# Require npm package releases to be at least 24 hours old before install.", - `min-release-age=${NPM_MIN_RELEASE_AGE_DAYS}`, - "", - ].join("\n"); -} - -function createDefaultSettings(request: InitRequest): ProofKitSettings { - return { - ui: request.ui, - appType: request.appType, - envFile: ".env", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], - }; -} - -function createEnvFileContent() { - return ["# When adding additional environment variables, update the schema alongside this file.", ""].join("\n"); -} - -const sharedUiDependencies = { - "@radix-ui/react-slot": "^1.2.3", - "class-variance-authority": "^0.7.1", - clsx: "^2.1.1", - "lucide-react": "^1.16.0", - "tailwind-merge": "^3.5.0", - tailwindcss: "^4.1.10", - "tw-animate-css": "^1.4.0", -} satisfies Record; - -export function planInit( - request: InitRequest, - options: { templateDir: string; packageManagerVersion?: string }, -): InitPlan { - const targetDir = path.resolve(request.cwd, request.appDir); - const proofkitFmdapiVersion = getProofkitDependencyVersion(getFmdapiVersion()); - const proofkitTypegenVersion = getProofkitDependencyVersion(getTypegenVersion()); - const proofkitWebviewerVersion = getProofkitDependencyVersion(getProofkitWebviewerVersion()); - const settings = createDefaultSettings(request); - const packageManagerCommand = getTemplatePackageCommand(request.packageManager); - const packageManagerExecuteCommand = getTemplatePackageExecuteCommand(request.packageManager); - const shouldWritePnpmWorkspaceFile = request.packageManager === "pnpm"; - const shouldWriteNpmrcFile = request.packageManager === "npm"; - - const packageJson: InitPlan["packageJson"] = { - name: request.scopedAppName, - engines: { - node: NODE_RUNTIME_VERSION, - }, - devEngines: options.packageManagerVersion - ? { - packageManager: { - name: request.packageManager, - version: createPackageManagerVersionRange(options.packageManagerVersion), - onFail: "download", - }, - runtime: { - name: "node", - version: NODE_RUNTIME_VERSION, - onFail: "download", - }, - } - : undefined, - proofkitMetadata: { - initVersion: getScaffoldVersion(), - scaffoldPackage: "@proofkit/cli", - }, - dependencies: {}, - devDependencies: { - "@types/node": `^${getNodeMajorVersion()}`, - }, - }; - - if (request.appType === "browser") { - packageJson.devDependencies["@proofkit/typegen"] = proofkitTypegenVersion; - packageJson.devDependencies.oxlint = "^1.39.0"; - Object.assign(packageJson.dependencies, sharedUiDependencies); - packageJson.dependencies["@tailwindcss/postcss"] = "^4.1.10"; - packageJson.dependencies["next-themes"] = "^0.4.6"; - if (request.dataSource === "filemaker") { - packageJson.dependencies["@proofkit/fmdapi"] = proofkitFmdapiVersion; - packageJson.dependencies.zod = "^4"; - } - } - - if (request.appType === "webviewer") { - Object.assign(packageJson.dependencies, sharedUiDependencies); - packageJson.dependencies["@proofkit/fmdapi"] = proofkitFmdapiVersion; - packageJson.dependencies["@proofkit/webviewer"] = proofkitWebviewerVersion; - packageJson.dependencies["@tanstack/react-query"] = "^5.90.21"; - packageJson.dependencies["@tanstack/react-router"] = "^1.167.4"; - packageJson.dependencies.zod = "^4"; - packageJson.devDependencies["@proofkit/typegen"] = proofkitTypegenVersion; - packageJson.devDependencies["@tailwindcss/vite"] = "^4.2.1"; - packageJson.devDependencies.oxlint = "^1.39.0"; - packageJson.devDependencies.ultracite = "^7.0.0"; - } - - const shouldRunInitialCodegen = - !request.noInstall && - request.dataSource === "filemaker" && - !request.skipFileMakerSetup && - !(request.appType === "webviewer" && request.nonInteractive && !request.hasExplicitFileMakerInputs); - - return { - request, - targetDir, - templateDir: options.templateDir, - packageManagerCommand, - packageManagerExecuteCommand, - packageJson, - settings, - envFile: { - path: path.join(targetDir, ".env"), - content: createEnvFileContent(), - }, - writes: [ - { - path: path.join(targetDir, ".cursorignore"), - content: "CLAUDE.md\n", - }, - ...(shouldWritePnpmWorkspaceFile - ? [ - { - path: path.join(targetDir, "pnpm-workspace.yaml"), - content: createPnpmWorkspaceFileContent(request.appType), - }, - ] - : []), - ...(shouldWriteNpmrcFile - ? [ - { - path: path.join(targetDir, ".npmrc"), - content: createNpmrcFileContent(), - }, - ] - : []), - ], - commands: [ - ...(request.noInstall ? [] : [{ type: "install" as const }]), - { type: "ultracite-init" as const }, - ...(request.noInstall ? [] : [{ type: "intent-install" as const }]), - ...(shouldRunInitialCodegen ? [{ type: "codegen" as const }] : []), - ...(request.noInstall ? [] : [{ type: "fix" as const }]), - ...(request.noInstall ? [] : [{ type: "lint" as const }]), - ...(request.noGit ? [] : [{ type: "git-init" as const }]), - ], - tasks: { - bootstrapFileMaker: request.dataSource === "filemaker" && !request.skipFileMakerSetup, - checkWebViewerAddon: request.appType === "webviewer", - runInstall: !request.noInstall, - runUltraciteInit: true, - runIntentInstall: !request.noInstall, - runInitialCodegen: shouldRunInitialCodegen, - runFix: !request.noInstall, - runLint: !request.noInstall, - initializeGit: !request.noGit, - }, - nextSteps: [ - `cd ${request.appDir}`, - ...(request.packageManager === "npm" ? [NPM_PACKAGE_MANAGER_WARNING] : []), - ...(request.noInstall ? [request.packageManager === "yarn" ? "yarn" : `${request.packageManager} install`] : []), - formatPackageManagerCommand(request.packageManager, "dev"), - ...(request.appType === "webviewer" - ? [ - formatPackageManagerCommand(request.packageManager, "typegen"), - formatPackageManagerCommand(request.packageManager, "launch-fm"), - ] - : []), - ], - }; -} - -export function applyPackageJsonMutations( - packageJson: PackageJson, - mutations: InitPlan["packageJson"], - overwriteDependencies = true, -) { - packageJson.name = mutations.name; - packageJson.proofkitMetadata = mutations.proofkitMetadata as PackageJson["proofkitMetadata"]; - if (mutations.devEngines) { - packageJson.devEngines = mutations.devEngines; - packageJson.packageManager = undefined; - } - packageJson.engines = mutations.engines as PackageJson["engines"]; - - if (!packageJson.dependencies) { - packageJson.dependencies = {}; - } - if (!packageJson.devDependencies) { - packageJson.devDependencies = {}; - } - - const merge = (target: Record, source: Record) => { - for (const [name, version] of Object.entries(source)) { - if (overwriteDependencies || !(name in target)) { - target[name] = version; - } - } - }; - - merge(packageJson.dependencies as Record, mutations.dependencies); - merge(packageJson.devDependencies as Record, mutations.devDependencies); - - return packageJson; -} diff --git a/packages/cli/src/core/prompt.ts b/packages/cli/src/core/prompt.ts deleted file mode 100644 index 386ae8f4..00000000 --- a/packages/cli/src/core/prompt.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Effect } from "effect"; -import { DOCS_URL } from "~/consts.js"; -import { ConsoleService } from "~/core/context.js"; - -export const runPrompt = Effect.gen(function* () { - const consoleService = yield* ConsoleService; - - consoleService.note( - [ - "Agent-ready prompts are coming soon.", - "", - "This command will become the stable entrypoint for docs-linked AI workflows.", - `For now, use package-native tools directly and check docs: ${DOCS_URL}/docs/cli`, - ].join("\n"), - "Coming soon", - ); -}); diff --git a/packages/cli/src/core/resolveInitRequest.ts b/packages/cli/src/core/resolveInitRequest.ts deleted file mode 100644 index 6c6ae5b3..00000000 --- a/packages/cli/src/core/resolveInitRequest.ts +++ /dev/null @@ -1,801 +0,0 @@ -import { Effect } from "effect"; - -import { DEFAULT_APP_NAME } from "~/consts.js"; -import { CliContext, ConsoleService, FileMakerService, PackageManagerService, PromptService } from "~/core/context.js"; -import { - CliValidationError, - FileMakerSetupError, - isCliError, - NonInteractiveInputError, - UserCancelledError, -} from "~/core/errors.js"; -import type { AppType, CliFlags, DataSourceType, FileMakerInputs, InitRequest } from "~/core/types.js"; -import { createDataSourceEnvNames, getDefaultSchemaName } from "~/utils/projectFiles.js"; -import { parseNameAndPath, validateAppName } from "~/utils/projectName.js"; - -const defaultFlags: CliFlags = { - noGit: false, - noInstall: false, - force: false, - default: false, - CI: false, - importAlias: "~/", -}; - -function compareSemver(left: string, right: string) { - const leftParts = left.split(".").map((part) => Number.parseInt(part, 10) || 0); - const rightParts = right.split(".").map((part) => Number.parseInt(part, 10) || 0); - - for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) { - const leftValue = leftParts[index] ?? 0; - const rightValue = rightParts[index] ?? 0; - if (leftValue > rightValue) { - return 1; - } - if (leftValue < rightValue) { - return -1; - } - } - - return 0; -} - -function resolveLayoutNameMatch(layouts: string[], requestedLayoutName: string) { - const exactMatch = layouts.find((layout) => layout === requestedLayoutName); - if (exactMatch) { - return exactMatch; - } - - const normalizedRequestedLayoutName = requestedLayoutName.toLocaleLowerCase(); - return layouts.find((layout) => layout.toLocaleLowerCase() === normalizedRequestedLayoutName); -} - -function validateLayoutInputs(flags: CliFlags) { - const hasLayoutName = Boolean(flags.layoutName); - const hasSchemaName = Boolean(flags.schemaName); - - if (hasLayoutName !== hasSchemaName) { - return Effect.fail( - new CliValidationError({ - message: "Both --layout-name and --schema-name must be provided together.", - }), - ); - } - - return Effect.void; -} - -function promptEffect(message: string, run: () => Promise) { - return Effect.tryPromise({ - try: run, - catch: (cause) => - isCliError(cause) - ? cause - : new CliValidationError({ - message, - cause, - }), - }); -} - -function getMissingFlags(values: [flag: string, value: unknown][]) { - return values.filter(([, value]) => !value).map(([flag]) => flag); -} - -function createMissingInputsMessage(scope: string, flags: string[]) { - return `Missing required ${scope} inputs in non-interactive mode: ${flags.join(", ")}.`; -} - -function resolvePackageManager({ - cwd, - packageManager, - nonInteractive, -}: { - cwd: string; - packageManager: "npm" | "pnpm" | "yarn" | "bun"; - nonInteractive: boolean; -}) { - return Effect.gen(function* () { - if (packageManager !== "npm") { - return packageManager; - } - - const packageManagerService = yield* PackageManagerService; - const prompt = yield* PromptService; - const pnpmVersionResult = yield* Effect.either(packageManagerService.getVersion("pnpm", cwd)); - if (pnpmVersionResult._tag === "Right") { - return "pnpm" as const; - } - - if (nonInteractive) { - return packageManager; - } - - const packageManagerChoice = yield* promptEffect("Unable to choose package manager.", () => - prompt.select<"abort" | "continue">({ - message: - "We strongly suggest you use PNPM instead of NPM to better secure yourself and the apps you build. https://pnpm.io/installation", - options: [ - { - value: "abort", - label: "Abort", - hint: "Install PNPM first", - }, - { - value: "continue", - label: "Continue with NPM", - hint: "Ignore this warning", - }, - ], - }), - ); - - if (packageManagerChoice === "abort") { - return yield* Effect.fail( - new UserCancelledError({ - message: "User aborted to install pnpm first.", - }), - ); - } - - return packageManager; - }); -} - -function resolveHostedFileMakerInputs({ - prompt, - fileMakerService, - flags, - nonInteractive, -}: { - prompt: PromptService; - fileMakerService: FileMakerService; - flags: CliFlags; - nonInteractive: boolean; -}) { - return Effect.gen(function* () { - yield* validateLayoutInputs(flags); - - if (!flags.server && nonInteractive) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: createMissingInputsMessage( - "hosted FileMaker", - getMissingFlags([ - ["--server", flags.server], - ["--file-name", flags.fileName], - ["--data-api-key", flags.dataApiKey], - ]), - ), - }), - ); - } - - const rawServer = - flags.server ?? - (yield* promptEffect("Unable to read FileMaker Server URL.", () => - prompt.text({ - message: "What is the URL of your FileMaker Server?", - validate: (value) => { - try { - const normalized = value.startsWith("http") ? value : `https://${value}`; - new URL(normalized); - return; - } catch { - return "Please enter a valid URL"; - } - }, - }), - )); - - const { normalizedUrl, versions } = yield* fileMakerService.validateHostedServerUrl(rawServer); - const hostedUrl = new URL(normalizedUrl); - const demoFileName = "ProofKitDemo.fmp12"; - - let selectedFile = flags.fileName; - let dataApiKey = flags.dataApiKey; - let layoutName = flags.layoutName; - let schemaName = flags.schemaName; - let token: string | undefined; - let files: Array<{ filename: string; status: string }> = []; - - const requireHostedToken = () => - token - ? Effect.succeed(token) - : Effect.fail( - new FileMakerSetupError({ - message: "OttoFMS authentication is required for hosted setup.", - }), - ); - - if (!(selectedFile && dataApiKey)) { - if (!(flags.adminApiKey || (versions.ottoVersion && compareSemver(versions.ottoVersion, "4.7.0") >= 0))) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: - "OttoFMS 4.7.0 or later is required to auto-login. Upgrade OttoFMS or pass --admin-api-key for hosted setup.", - }), - ); - } - token = flags.adminApiKey ?? (yield* fileMakerService.getOttoFMSToken({ url: hostedUrl })).token; - } - - if (!selectedFile) { - if (nonInteractive) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: createMissingInputsMessage( - "FileMaker", - getMissingFlags([ - ["--file-name", selectedFile], - ["--data-api-key", dataApiKey], - ]), - ), - }), - ); - } - - files = yield* fileMakerService.listFiles({ - url: hostedUrl, - token: yield* requireHostedToken(), - }); - selectedFile = yield* promptEffect("Unable to choose a FileMaker file.", () => - prompt.searchSelect({ - message: "Which file would you like to connect to?", - options: [ - { - value: "$deploy-demo", - label: "Deploy NEW ProofKit Demo File", - hint: "Use OttoFMS to deploy a new file for testing", - keywords: ["demo", "proofkit"], - }, - ...files - .slice() - .sort((left, right) => left.filename.localeCompare(right.filename)) - .map((file) => ({ - value: file.filename, - label: file.filename, - hint: file.status, - keywords: [file.filename], - })), - ], - }), - ); - } - - if (!selectedFile) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: "No FileMaker file was selected.", - }), - ); - } - - if (selectedFile === "$deploy-demo") { - if (files.length === 0) { - files = yield* fileMakerService.listFiles({ - url: hostedUrl, - token: yield* requireHostedToken(), - }); - } - const demoExists = files.some((file) => file.filename === demoFileName); - const replaceDemo = - demoExists && !nonInteractive - ? yield* promptEffect("Unable to confirm ProofKit Demo replacement.", () => - prompt.confirm({ - message: "The demo file already exists. Do you want to replace it with a fresh copy?", - initialValue: false, - }), - ) - : demoExists; - const deployed = yield* fileMakerService.deployDemoFile({ - url: hostedUrl, - token: yield* requireHostedToken(), - operation: replaceDemo ? "replace" : "install", - }); - selectedFile = deployed.filename; - dataApiKey = deployed.apiKey; - layoutName ??= "API_Contacts"; - schemaName ??= "Contacts"; - } - - if (!dataApiKey && nonInteractive) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: createMissingInputsMessage("FileMaker", getMissingFlags([["--data-api-key", dataApiKey]])), - }), - ); - } - - if (!dataApiKey) { - const apiKeys = (yield* fileMakerService.listAPIKeys({ - url: hostedUrl, - token: yield* requireHostedToken(), - })).filter((apiKey: { database: string }) => apiKey.database === selectedFile); - - const selection = - apiKeys.length === 0 - ? "create" - : yield* promptEffect("Unable to choose an OttoFMS Data API key.", () => - prompt.searchSelect({ - message: "Which OttoFMS Data API key would you like to use?", - options: [ - ...apiKeys.map((apiKey: { key: string; label: string; user: string; database: string }) => ({ - value: apiKey.key, - label: `${apiKey.label} - ${apiKey.user}`, - hint: `${apiKey.key.slice(0, 5)}...${apiKey.key.slice(-4)}`, - keywords: [apiKey.label, apiKey.user, apiKey.database], - })), - { - value: "create", - label: "Create a new API key", - hint: "Requires FileMaker credentials for this file", - keywords: ["create", "new"], - }, - ], - }), - ); - - if (selection === "create") { - const username = yield* promptEffect("Unable to read FileMaker account name.", () => - prompt.text({ - message: `Enter the account name for ${selectedFile}`, - validate: (value) => (value ? undefined : "An account name is required"), - }), - ); - const password = yield* promptEffect("Unable to read FileMaker account password.", () => - prompt.password({ - message: `Enter the password for ${username}`, - validate: (value) => (value ? undefined : "A password is required"), - }), - ); - if (!selectedFile) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: "No FileMaker file was selected.", - }), - ); - } - dataApiKey = (yield* fileMakerService.createDataAPIKeyWithCredentials({ - url: hostedUrl, - filename: selectedFile, - username, - password, - })).apiKey; - } else { - dataApiKey = selection; - } - } - - if (!dataApiKey) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: "No FileMaker Data API key was selected.", - }), - ); - } - - const resolvedFileName = selectedFile; - if (!resolvedFileName) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: "No FileMaker file was selected.", - }), - ); - } - - const layouts = yield* fileMakerService.listLayouts({ - dataApiKey, - fmFile: resolvedFileName, - server: hostedUrl.origin, - }); - - if (layoutName) { - const matchedLayoutName = resolveLayoutNameMatch(layouts, layoutName); - if (!matchedLayoutName) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: `Layout "${layoutName}" was not found in ${resolvedFileName}.`, - }), - ); - } - - layoutName = matchedLayoutName; - } - - if (!(nonInteractive || layoutName || schemaName)) { - const shouldConfigureLayout = yield* promptEffect("Unable to confirm initial layout setup.", () => - prompt.confirm({ - message: "Do you want to configure an initial layout for type generation now?", - initialValue: false, - }), - ); - - if (shouldConfigureLayout) { - layoutName = yield* promptEffect("Unable to choose a FileMaker layout.", () => - prompt.searchSelect({ - message: "Select a layout to read data from", - options: layouts.map((layout: string) => ({ - value: layout, - label: layout, - keywords: [layout], - })), - }), - ); - - const resolvedLayoutName = layoutName; - if (!resolvedLayoutName) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: "No FileMaker layout was selected.", - }), - ); - } - schemaName = yield* promptEffect("Unable to read generated schema name.", () => - prompt.text({ - message: "What should the generated schema be called?", - defaultValue: getDefaultSchemaName(resolvedLayoutName), - validate: (value) => (value ? undefined : "A schema name is required"), - }), - ); - } - } - - if (!selectedFile) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: "No FileMaker file was selected.", - }), - ); - } - if (!dataApiKey) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: "No FileMaker Data API key was selected.", - }), - ); - } - - return { - mode: "hosted-otto", - dataSourceName: "filemaker", - envNames: createDataSourceEnvNames("filemaker"), - server: hostedUrl.origin, - fileName: selectedFile, - dataApiKey, - layoutName, - schemaName, - adminApiKey: flags.adminApiKey, - fmsVersion: versions.fmsVersion, - ottoVersion: versions.ottoVersion, - } satisfies FileMakerInputs; - }); -} - -function resolveFileMakerInputs({ - prompt, - console, - fileMakerService, - flags, - appType, - nonInteractive, - projectName, - proofkitToken, -}: { - prompt: PromptService; - console: ConsoleService; - fileMakerService: FileMakerService; - flags: CliFlags; - appType: AppType; - nonInteractive: boolean; - projectName: string; - proofkitToken?: string; -}) { - return Effect.gen(function* () { - if (flags.dataSource !== "filemaker") { - return { fileMaker: undefined, skipFileMakerSetup: false }; - } - - yield* validateLayoutInputs(flags); - - if (appType === "webviewer" && !flags.server) { - const resolveLocalFmMcpFile = (connectedFiles: string[]) => - Effect.gen(function* () { - const availableFiles = connectedFiles.filter(Boolean); - if (availableFiles.length === 0) { - return undefined; - } - - if (flags.fileName) { - if (availableFiles.includes(flags.fileName)) { - return flags.fileName; - } - - return yield* Effect.fail( - new FileMakerSetupError({ - message: `FileMaker file "${flags.fileName}" is not currently connected to the ProofKit plugin. Connected files: ${availableFiles.join(", ")}.`, - }), - ); - } - - if (availableFiles.length === 1) { - return availableFiles[0]; - } - - if (nonInteractive) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: `Multiple FileMaker files are connected to the ProofKit plugin. Pass --file-name with one of: ${availableFiles.join(", ")}.`, - }), - ); - } - - return yield* promptEffect("Unable to choose a local FileMaker file.", () => - prompt.searchSelect({ - message: "Multiple FileMaker files are open. Which file should ProofKit use?", - options: availableFiles.map((fileName) => ({ - value: fileName, - label: fileName, - hint: "Connected via ProofKit plugin", - keywords: [fileName], - })), - }), - ); - }); - - while (true) { - const localFmMcp = yield* fileMakerService.detectLocalFmMcp(); - yield* fileMakerService.installLocalWebViewerAddon(); - const selectedFile = localFmMcp.healthy ? yield* resolveLocalFmMcpFile(localFmMcp.connectedFiles) : undefined; - if (localFmMcp.healthy && selectedFile) { - if (!(nonInteractive || proofkitToken)) { - yield* fileMakerService.authorizeLocalFmMcp({ - baseUrl: localFmMcp.baseUrl, - fileName: selectedFile, - interactive: true, - clientName: `ProofKit CLI (${projectName})`, - clientDescription: - "ProofKit CLI wants to read layouts from your FileMaker file to help set up your project.", - }); - } - console.info(`Using ProofKit plugin file: ${selectedFile}`); - return { - fileMaker: { - mode: "local-fm-mcp", - dataSourceName: "filemaker", - envNames: createDataSourceEnvNames("filemaker"), - fmMcpBaseUrl: localFmMcp.baseUrl, - fileName: selectedFile, - layoutName: flags.layoutName, - schemaName: flags.schemaName, - proofkitToken, - } satisfies FileMakerInputs, - skipFileMakerSetup: false, - }; - } - - if (nonInteractive) { - if (localFmMcp.healthy) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: - "ProofKit plugin was detected, but no FileMaker file is connected. Install the ProofKit plugin, install the ProofKit Web Viewer add-on in your FileMaker file, then run the add-on connection script and rerun. Or pass --server.", - }), - ); - } - - return yield* Effect.fail( - new NonInteractiveInputError({ - message: - "ProofKit plugin was not detected and no FileMaker server was provided. Install the ProofKit plugin, then rerun. Or pass --server.", - }), - ); - } - - const fallbackAction = yield* promptEffect("Unable to choose FileMaker setup fallback.", () => - prompt.select({ - message: localFmMcp.healthy - ? "ProofKit plugin is installed, but no FileMaker file is connected yet. Install the ProofKit Web Viewer add-on in your FileMaker file, run the add-on connection script, then choose how to continue." - : "ProofKit plugin was not detected. How would you like to continue?", - options: [ - { - value: "retry", - label: "Try again", - hint: localFmMcp.healthy - ? "Check again after opening a FileMaker file" - : "Retry ProofKit plugin detection", - }, - { - value: "hosted", - label: "Continue with hosted setup", - hint: "Use OttoFMS and a hosted FileMaker server", - }, - { - value: "skip", - label: "Skip for now", - hint: "Create the project and configure FileMaker later", - }, - ], - }), - ); - - if (fallbackAction === "retry") { - continue; - } - - if (fallbackAction === "skip") { - return { - fileMaker: undefined, - skipFileMakerSetup: true, - }; - } - - break; - } - } - - return { - fileMaker: yield* resolveHostedFileMakerInputs({ - prompt, - fileMakerService, - flags, - nonInteractive, - }), - skipFileMakerSetup: false, - }; - }); -} - -export const resolveInitRequest = (name?: string, rawFlags?: CliFlags) => - Effect.gen(function* () { - const flags = { ...defaultFlags, ...rawFlags }; - const proofkitToken = flags.proofkitToken?.trim() || process.env.FM_MCP_SESSION_ID?.trim(); - const prompt = yield* PromptService; - const console = yield* ConsoleService; - const fileMakerService = yield* FileMakerService; - const cliContext = yield* CliContext; - const nonInteractive = cliContext.nonInteractive || flags.CI || flags.nonInteractive === true; - const packageManager = yield* resolvePackageManager({ - cwd: cliContext.cwd, - packageManager: cliContext.packageManager, - nonInteractive, - }); - - let projectName = name; - if (!projectName) { - if (nonInteractive) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: "Project name is required in non-interactive mode.", - }), - ); - } - - projectName = yield* promptEffect("Unable to read project name.", () => - prompt.text({ - message: "What will your project be called?", - defaultValue: DEFAULT_APP_NAME, - validate: validateAppName, - }), - ); - } - - if (!projectName) { - return yield* Effect.fail( - new CliValidationError({ - message: "Project name is required.", - }), - ); - } - - const validationError = validateAppName(projectName); - if (validationError) { - return yield* Effect.fail( - new CliValidationError({ - message: validationError, - }), - ); - } - - let appType: AppType = flags.appType ?? "browser"; - if (!(flags.appType || nonInteractive)) { - appType = yield* promptEffect("Unable to choose app type.", () => - prompt.select({ - message: "What kind of app do you want to build?", - options: [ - { - value: "browser", - label: "Web App for Browsers", - hint: "Uses Next.js and hosted deployment", - }, - { - value: "webviewer", - label: "FileMaker Web Viewer", - hint: "Uses Vite for FileMaker web viewers", - }, - ], - }), - ); - } - - const hasExplicitFileMakerInputs = Boolean( - flags.server || flags.adminApiKey || flags.dataApiKey || flags.fileName || flags.layoutName || flags.schemaName, - ); - - let dataSource: DataSourceType = "none"; - if (flags.dataSource) { - dataSource = flags.dataSource; - } else if (appType === "webviewer") { - dataSource = hasExplicitFileMakerInputs || !(nonInteractive && !flags.server) ? "filemaker" : "none"; - } - - if (!(nonInteractive || flags.dataSource) && appType !== "webviewer") { - dataSource = yield* promptEffect("Unable to choose data source setup.", () => - prompt.select({ - message: "Do you want to connect to a FileMaker Database now?", - options: [ - { - value: "filemaker", - label: "Yes", - hint: "Set up env, datasource config, and typegen now", - }, - { - value: "none", - label: "No", - hint: "You can add a data source later", - }, - ], - }), - ); - } - - if (nonInteractive && !flags.dataSource && hasExplicitFileMakerInputs) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: "FileMaker flags require --data-source filemaker in non-interactive mode.", - }), - ); - } - - if (nonInteractive && dataSource !== "filemaker" && hasExplicitFileMakerInputs) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: "FileMaker flags require --data-source filemaker in non-interactive mode.", - }), - ); - } - - const [scopedAppName, appDir] = parseNameAndPath(projectName); - - const { fileMaker, skipFileMakerSetup } = yield* resolveFileMakerInputs({ - prompt, - console, - fileMakerService, - flags: { ...flags, dataSource }, - appType, - nonInteractive, - projectName: scopedAppName, - proofkitToken, - }); - - return { - projectName, - scopedAppName, - appDir, - appType, - ui: flags.ui ?? "shadcn", - dataSource, - packageManager, - noInstall: flags.noInstall, - noGit: flags.noGit, - force: flags.force, - cwd: cliContext.cwd, - importAlias: flags.importAlias, - nonInteractive, - debug: cliContext.debug, - proofkitToken, - fileMaker, - skipFileMakerSetup, - hasExplicitFileMakerInputs, - } satisfies InitRequest; - }); diff --git a/packages/cli/src/core/types.ts b/packages/cli/src/core/types.ts deleted file mode 100644 index bd49f71e..00000000 --- a/packages/cli/src/core/types.ts +++ /dev/null @@ -1,168 +0,0 @@ -import type { PackageManager } from "~/utils/packageManager.js"; - -export type AppType = "browser" | "webviewer"; -export type UIType = "shadcn" | "mantine"; -export type DataSourceType = "filemaker" | "none"; -export type OverwriteMode = "overwrite" | "clear"; -export type FileMakerMode = "hosted-otto" | "local-fm-mcp"; - -export interface CliFlags { - noGit: boolean; - noInstall: boolean; - force: boolean; - default: boolean; - importAlias: string; - debug?: boolean; - server?: string; - adminApiKey?: string; - fileName?: string; - layoutName?: string; - schemaName?: string; - dataApiKey?: string; - auth?: "none"; - dataSource?: DataSourceType; - ui?: UIType; - CI: boolean; - nonInteractive?: boolean; - appType?: AppType; - proofkitToken?: string; -} - -export interface FileMakerEnvNames { - database: string; - server: string; - apiKey: string; -} - -export interface HostedFileMakerInputs { - mode: "hosted-otto"; - dataSourceName: string; - envNames: FileMakerEnvNames; - server: string; - fileName: string; - dataApiKey: string; - layoutName?: string; - schemaName?: string; - adminApiKey?: string; - fmsVersion?: string; - ottoVersion?: string | null; -} - -export interface LocalFmMcpInputs { - mode: "local-fm-mcp"; - dataSourceName: string; - envNames: FileMakerEnvNames; - fmMcpBaseUrl: string; - fileName: string; - layoutName?: string; - schemaName?: string; - proofkitToken?: string; -} - -export type FileMakerInputs = HostedFileMakerInputs | LocalFmMcpInputs; - -export interface InitRequest { - projectName: string; - scopedAppName: string; - appDir: string; - appType: AppType; - ui: UIType; - dataSource: DataSourceType; - packageManager: PackageManager; - noInstall: boolean; - noGit: boolean; - force: boolean; - cwd: string; - importAlias: string; - nonInteractive: boolean; - debug: boolean; - proofkitToken?: string; - skipFileMakerSetup: boolean; - fileMaker?: FileMakerInputs; - hasExplicitFileMakerInputs: boolean; -} - -export interface ProofKitSettings { - ui: UIType; - appType: AppType; - envFile?: string; - dataSources: Array<{ - type: "fm"; - name: string; - envNames: { - database: string; - server: string; - apiKey: string; - }; - }>; - replacedMainPage: boolean; - registryTemplates: string[]; -} - -export interface InitPlan { - request: InitRequest; - targetDir: string; - templateDir: string; - overwriteMode?: OverwriteMode; - packageManagerCommand: string; - packageManagerExecuteCommand: string; - packageJson: { - name: string; - devEngines?: { - packageManager: { - name: PackageManager; - version: string; - onFail: "download"; - }; - runtime: { - name: "node"; - version: string; - onFail: "download"; - }; - }; - engines: { - node: string; - }; - proofkitMetadata: { - initVersion: string; - scaffoldPackage: "@proofkit/cli"; - }; - dependencies: Record; - devDependencies: Record; - }; - settings: ProofKitSettings; - envFile: { - path: string; - content: string; - }; - writes: Array<{ - path: string; - content: string; - }>; - commands: Array< - | { type: "install" } - | { type: "ultracite-init" } - | { type: "intent-install" } - | { type: "codegen" } - | { type: "fix" } - | { type: "lint" } - | { type: "git-init" } - >; - tasks: { - bootstrapFileMaker: boolean; - checkWebViewerAddon: boolean; - runInstall: boolean; - runUltraciteInit: boolean; - runIntentInstall: boolean; - runInitialCodegen: boolean; - runFix: boolean; - runLint: boolean; - initializeGit: boolean; - }; - nextSteps: string[]; -} - -export interface InitResult { - request: InitRequest; - plan: InitPlan; -} diff --git a/packages/cli/src/generators/auth.ts b/packages/cli/src/generators/auth.ts deleted file mode 100644 index 7c366c9f..00000000 --- a/packages/cli/src/generators/auth.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { readFileSync, writeFileSync } from "node:fs"; -import path from "node:path"; -import { glob } from "glob"; - -import { installDependencies } from "~/helpers/installDependencies.js"; -import { betterAuthInstaller } from "~/installers/better-auth.js"; -import { clerkInstaller } from "~/installers/clerk.js"; -import { proofkitAuthInstaller } from "~/installers/proofkit-auth.js"; -import { state } from "~/state.js"; -import { getSettings, mergeSettings } from "~/utils/parseSettings.js"; - -export async function addAuth({ - options, - noInstall = false, - projectDir = process.cwd(), -}: { - options: - | { type: "clerk" } - | { - type: "fmaddon"; - emailProvider?: "plunk" | "resend"; - apiKey?: string; - } - | { type: "better-auth" }; - projectDir?: string; - noInstall?: boolean; -}) { - const settings = getSettings(); - if (settings.ui === "shadcn") { - throw new Error("`proofkit add auth` is no longer supported for shadcn projects"); - } - if (settings.auth.type !== "none") { - throw new Error("Auth already exists"); - } - if (!settings.dataSources.some((o) => o.type === "fm") && options.type === "fmaddon") { - throw new Error("A FileMaker data source is required to use the FM Add-on Auth"); - } - if (!settings.dataSources.some((o) => o.type === "fm") && options.type === "better-auth") { - throw new Error("A FileMaker data source is required to use the Better-Auth"); - } - - if (options.type === "clerk") { - await addClerkAuth({ projectDir }); - } else if (options.type === "fmaddon") { - await addFmaddonAuth(); - } - - // Replace actionClient with authedActionClient in all action files - await replaceActionClientWithAuthed(); - - if (!noInstall) { - await installDependencies({ projectDir }); - } -} - -async function addClerkAuth({ projectDir = process.cwd() }: { projectDir?: string }) { - await clerkInstaller({ projectDir }); - mergeSettings({ auth: { type: "clerk" } }); -} - -async function addFmaddonAuth() { - await proofkitAuthInstaller(); - mergeSettings({ auth: { type: "fmaddon" } }); -} - -async function replaceActionClientWithAuthed() { - const projectDir = state.projectDir; - const actionFiles = await glob("src/app/(main)/**/actions.ts", { - cwd: projectDir, - }); - - for (const file of actionFiles) { - const fullPath = path.join(projectDir, file); - const content = readFileSync(fullPath, "utf-8"); - const updatedContent = content.replace(/actionClient/g, "authedActionClient"); - writeFileSync(fullPath, updatedContent); - } -} - -async function _addBetterAuth() { - await betterAuthInstaller(); - mergeSettings({ auth: { type: "better-auth" } }); -} diff --git a/packages/cli/src/generators/fmdapi.ts b/packages/cli/src/generators/fmdapi.ts deleted file mode 100644 index 27602c99..00000000 --- a/packages/cli/src/generators/fmdapi.ts +++ /dev/null @@ -1,525 +0,0 @@ -import path from "node:path"; -import { generateTypedClients } from "@proofkit/typegen"; -import type { typegenConfigSingle } from "@proofkit/typegen/config"; -import { config as dotenvConfig } from "dotenv"; -import fs from "fs-extra"; -import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser/lib/esm/main.js"; -import { SyntaxKind } from "ts-morph"; -import type { z } from "zod/v4"; - -import { state } from "~/state.js"; -import { logger } from "~/utils/logger.js"; -import type { envNamesSchema } from "~/utils/parseSettings.js"; -import { getNewProject } from "~/utils/ts-morph.js"; - -// Input schema for functions like addLayout -// This might be different from the layout config stored in the file -interface Schema { - layoutName: string; - schemaName: string; - valueLists?: "strict" | "allowEmpty" | "ignore"; - generateClient?: boolean; - strictNumbers?: boolean; -} - -// For any data source configuration object (fmdapi or fmodata) -type AnyDataSourceConfig = z.infer; -// For a single fmdapi data source configuration object -type FmdapiDataSourceConfig = Extract; -// For a single layout configuration object within a data source -type ImportedLayoutConfig = FmdapiDataSourceConfig["layouts"][number]; - -// This type represents the actual structure of the JSONC file, including $schema -interface FullProofkitTypegenJsonFile { - $schema?: string; - config: AnyDataSourceConfig | AnyDataSourceConfig[]; -} - -const typegenConfigFileName = "proofkit-typegen.config.jsonc"; - -// Helper function to normalize data sources by adding default type for backwards compatibility -// This mirrors the zod preprocess in @proofkit/typegen that defaults type to "fmdapi" -function normalizeDataSource(ds: AnyDataSourceConfig): AnyDataSourceConfig { - if (!("type" in ds) || ds.type === undefined) { - return { ...(ds as object), type: "fmdapi" } as AnyDataSourceConfig; - } - return ds; -} - -function normalizeConfig( - config: AnyDataSourceConfig | AnyDataSourceConfig[], -): AnyDataSourceConfig | AnyDataSourceConfig[] { - if (Array.isArray(config)) { - return config.map(normalizeDataSource); - } - return normalizeDataSource(config); -} - -// Helper functions for JSON config -async function readJsonConfigFile(configPath: string): Promise { - if (!fs.existsSync(configPath)) { - return null; - } - try { - const fileContent = await fs.readFile(configPath, "utf8"); - const parsed = parseJsonc(fileContent) as FullProofkitTypegenJsonFile; - // Normalize config to add default type for backwards compatibility - if (parsed.config) { - parsed.config = normalizeConfig(parsed.config); - } - return parsed; - } catch (error) { - console.error(`Error reading or parsing JSONC config at ${configPath}:`, error); - // Return a default structure for the *file* if parsing fails but file exists - return { - $schema: "https://proofkit.proof.sh/typegen-config-schema.json", - config: [], - }; - } -} - -async function writeJsonConfigFile(configPath: string, fileContent: FullProofkitTypegenJsonFile) { - // Check if file exists to preserve comments - if (fs.existsSync(configPath)) { - const originalText = await fs.readFile(configPath, "utf8"); - // Use jsonc-parser's modify function to preserve comments - const edits = modify(originalText, ["config"], fileContent.config, { - formattingOptions: { - tabSize: 2, - insertSpaces: true, - eol: "\n", - }, - }); - const modifiedText = applyEdits(originalText, edits); - await fs.writeFile(configPath, modifiedText, "utf8"); - } else { - // If file doesn't exist, create it with proper formatting - await fs.writeJson(configPath, fileContent, { spaces: 2 }); - } -} - -export async function addLayout({ - projectDir = process.cwd(), - schemas, - runCodegen = true, - dataSourceName, -}: { - projectDir?: string; - schemas: Schema[]; - runCodegen?: boolean; - dataSourceName: string; -}) { - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - let fileContent = await readJsonConfigFile(jsonConfigPath); - - if (!fileContent) { - fileContent = { - $schema: "https://proofkit.proof.sh/typegen-config-schema.json", - config: [], - }; - } - - // Work with the 'config' property which is TypegenConfig['config'] - const configProperty = fileContent.config; - - let configArray: AnyDataSourceConfig[]; - if (Array.isArray(configProperty)) { - configArray = configProperty; - } else { - configArray = [configProperty]; - fileContent.config = configArray; // Update fileContent to ensure it's an array for later ops - } - - const layoutsToAdd: ImportedLayoutConfig[] = schemas.map((schema) => ({ - layoutName: schema.layoutName, - schemaName: schema.schemaName, - valueLists: schema.valueLists, - generateClient: schema.generateClient, - strictNumbers: schema.strictNumbers, - })); - - let targetDataSource: FmdapiDataSourceConfig | undefined = configArray.find( - (ds): ds is FmdapiDataSourceConfig => - ds.type === "fmdapi" && - (ds.path?.endsWith(dataSourceName) || ds.path?.endsWith(`${dataSourceName}/`) || ds.path === dataSourceName), - ); - - if (targetDataSource) { - targetDataSource.layouts = targetDataSource.layouts || []; - } else { - targetDataSource = { - type: "fmdapi", - layouts: [], - path: `./src/config/schemas/${dataSourceName}`, - // other default properties for a new DataSourceConfig can be added here if needed - envNames: undefined, - }; - configArray.push(targetDataSource); - } - - targetDataSource.layouts.push(...layoutsToAdd); - // fileContent.config is already pointing to configArray if it was modified - - await writeJsonConfigFile(jsonConfigPath, fileContent); - - if (runCodegen) { - await runCodegenCommand(); - } -} - -export async function addConfig({ - config, - projectDir, - runCodegen = true, -}: { - config: FmdapiDataSourceConfig | FmdapiDataSourceConfig[]; - projectDir: string; - runCodegen?: boolean; -}) { - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - let fileContent = await readJsonConfigFile(jsonConfigPath); - - const configsToAdd = Array.isArray(config) ? config : [config]; - - if (fileContent) { - if (Array.isArray(fileContent.config)) { - fileContent.config.push(...configsToAdd); - } else { - fileContent.config = [fileContent.config, ...configsToAdd]; - } - } else { - fileContent = { - $schema: "https://proofkit.proof.sh/typegen-config-schema.json", - config: configsToAdd, - }; - } - - await writeJsonConfigFile(jsonConfigPath, fileContent); - - if (runCodegen) { - await runCodegenCommand(); - } -} - -export async function ensureWebviewerFmMcpConfig({ - projectDir, - connectedFileName, - dataSourceName = "filemaker", - baseUrl, -}: { - projectDir: string; - connectedFileName?: string; - dataSourceName?: string; - baseUrl?: string; -}) { - const newConfig: FmdapiDataSourceConfig = { - type: "fmdapi", - path: `./src/config/schemas/${dataSourceName}`, - clearOldFiles: true, - clientSuffix: "Layout", - webviewerScriptName: "ExecuteDataApi", - envNames: undefined, - layouts: [], - fmMcp: { - enabled: true, - ...(baseUrl ? { baseUrl } : {}), - ...(connectedFileName ? { connectedFileName } : {}), - }, - }; - - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - let fileContent = await readJsonConfigFile(jsonConfigPath); - - if (!fileContent) { - fileContent = { - $schema: "https://proofkit.proof.sh/typegen-config-schema.json", - config: [newConfig], - }; - await writeJsonConfigFile(jsonConfigPath, fileContent); - return; - } - - const configArray = Array.isArray(fileContent.config) ? fileContent.config : [fileContent.config]; - if (!Array.isArray(fileContent.config)) { - fileContent.config = configArray; - } - - const existingConfigIndex = configArray.findIndex( - (config): config is FmdapiDataSourceConfig => config.type === "fmdapi" && config.path === newConfig.path, - ); - - if (existingConfigIndex === -1) { - configArray.push(newConfig); - } else { - const existingConfig = configArray[existingConfigIndex] as FmdapiDataSourceConfig; - configArray[existingConfigIndex] = { - ...existingConfig, - ...newConfig, - layouts: existingConfig.layouts ?? [], - fmMcp: { - enabled: true, - ...(existingConfig.fmMcp ?? {}), - ...(newConfig.fmMcp ?? {}), - }, - }; - } - - await writeJsonConfigFile(jsonConfigPath, fileContent); -} - -export async function runCodegenCommand() { - const projectDir = state.projectDir; - const config = await readJsonConfigFile(path.join(projectDir, typegenConfigFileName)); - if (!config) { - logger.info("no typegen config found, skipping typegen"); - return; - } - - // make sure to load the .env file - dotenvConfig({ path: path.join(projectDir, ".env") }); - await generateTypedClients(config.config, { cwd: projectDir }); -} - -export function getClientSuffix({ - projectDir = process.cwd(), - dataSourceName, -}: { - projectDir?: string; - dataSourceName: string; -}): string { - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - if (!fs.existsSync(jsonConfigPath)) { - return "Client"; - } - try { - const fileContent = fs.readFileSync(jsonConfigPath, "utf8"); - const parsed = parseJsonc(fileContent) as FullProofkitTypegenJsonFile; - - // Normalize config to add default type for backwards compatibility - const normalizedConfig = normalizeConfig(parsed.config); - const configToSearch = Array.isArray(normalizedConfig) ? normalizedConfig : [normalizedConfig]; - - const targetDataSource = configToSearch.find( - (ds): ds is FmdapiDataSourceConfig => - ds.type === "fmdapi" && - (ds.path?.endsWith(dataSourceName) || ds.path?.endsWith(`${dataSourceName}/`) || ds.path === dataSourceName), - ); - return targetDataSource?.clientSuffix ?? "Client"; - } catch (error) { - console.error(`Error reading or parsing JSONC config for getClientSuffix: ${jsonConfigPath}`, error); - return "Client"; - } -} - -export function getExistingSchemas({ - projectDir = process.cwd(), - dataSourceName, -}: { - projectDir?: string; - dataSourceName: string; -}): { layout?: string; schemaName?: string }[] { - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - if (!fs.existsSync(jsonConfigPath)) { - return []; - } - try { - const fileContent = fs.readFileSync(jsonConfigPath, "utf8"); - const parsed = parseJsonc(fileContent) as FullProofkitTypegenJsonFile; - - // Normalize config to add default type for backwards compatibility - const normalizedConfig = normalizeConfig(parsed.config); - const configToSearch = Array.isArray(normalizedConfig) ? normalizedConfig : [normalizedConfig]; - - const targetDataSource = configToSearch.find( - (ds): ds is FmdapiDataSourceConfig => - ds.type === "fmdapi" && - (ds.path?.endsWith(dataSourceName) || ds.path?.endsWith(`${dataSourceName}/`) || ds.path === dataSourceName), - ); - - if (targetDataSource?.layouts) { - return targetDataSource.layouts.map((layout) => ({ - layout: layout.layoutName, - schemaName: layout.schemaName, - })); - } - return []; - } catch (error) { - console.error(`Error reading or parsing JSONC config for getExistingSchemas: ${jsonConfigPath}`, error); - return []; - } -} - -export async function addToFmschemaConfig({ - dataSourceName, - envNames, -}: { - dataSourceName: string; - envNames?: z.infer; -}) { - const projectDir = state.projectDir; - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - let fileContent = await readJsonConfigFile(jsonConfigPath); - - const newDataSource: FmdapiDataSourceConfig = { - type: "fmdapi", - layouts: [], - path: `./src/config/schemas/${dataSourceName}`, - envNames: undefined, - clearOldFiles: true, - clientSuffix: "Layout", - }; - - if (envNames) { - newDataSource.envNames = { - server: envNames.server, - db: envNames.database, - auth: { apiKey: envNames.apiKey }, - }; - } - if (state.appType === "webviewer") { - newDataSource.webviewerScriptName = "ExecuteDataApi"; - } - - if (fileContent) { - let configArray: AnyDataSourceConfig[]; - if (Array.isArray(fileContent.config)) { - configArray = fileContent.config; - } else { - configArray = [fileContent.config]; - fileContent.config = configArray; - } - - const existingDsIndex = configArray.findIndex((ds) => ds.type === "fmdapi" && ds.path === newDataSource.path); - if (existingDsIndex === -1) { - configArray.push(newDataSource); - } else { - const existingConfig = configArray[existingDsIndex] as FmdapiDataSourceConfig; - configArray[existingDsIndex] = { - ...existingConfig, - ...newDataSource, - layouts: newDataSource.layouts.length > 0 ? newDataSource.layouts : existingConfig.layouts || [], - }; - } - } else { - fileContent = { - $schema: "https://proofkit.proof.sh/typegen-config-schema.json", - config: [newDataSource], - }; - } - await writeJsonConfigFile(jsonConfigPath, fileContent); -} - -export function getFieldNamesForSchema({ schemaName, dataSourceName }: { schemaName: string; dataSourceName: string }) { - const projectDir = state.projectDir; - const project = getNewProject(projectDir); - const sourceFilePath = path.join(projectDir, `src/config/schemas/${dataSourceName}/generated/${schemaName}.ts`); - - const sourceFilePathAlternative = path.join(projectDir, `src/config/schemas/${dataSourceName}/${schemaName}.ts`); - - let fileToUse = sourceFilePath; - if (!fs.existsSync(sourceFilePath)) { - if (fs.existsSync(sourceFilePathAlternative)) { - fileToUse = sourceFilePathAlternative; - } else { - return []; - } - } - const sourceFile = project.addSourceFileAtPath(fileToUse); - - const zodSchema = sourceFile.getVariableDeclaration(`Z${schemaName}`); - if (zodSchema) { - const properties = zodSchema - .getInitializer() - ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression) - ?.getProperties(); - return ( - properties?.map((pr) => pr.asKind(SyntaxKind.PropertyAssignment)?.getName()?.replace(/"/g, "")).filter(Boolean) ?? - [] - ); - } - const typeAlias = sourceFile.getTypeAlias(`T${schemaName}`); - const properties = typeAlias?.getFirstDescendantByKind(SyntaxKind.TypeLiteral)?.getProperties(); - return ( - properties?.map((pr) => pr.asKind(SyntaxKind.PropertySignature)?.getName()?.replace(/"/g, "")).filter(Boolean) ?? [] - ); -} - -export async function removeFromFmschemaConfig({ dataSourceName }: { dataSourceName: string }) { - const projectDir = state.projectDir; - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - const fileContent = await readJsonConfigFile(jsonConfigPath); - - if (!fileContent) { - return; - } - - const pathToRemove = `./src/config/schemas/${dataSourceName}`; - - if (Array.isArray(fileContent.config)) { - fileContent.config = fileContent.config.filter((ds) => !(ds.type === "fmdapi" && ds.path === pathToRemove)); - } else { - const currentConfig = fileContent.config; - if (currentConfig.type === "fmdapi" && currentConfig.path === pathToRemove) { - fileContent.config = []; - } - } - await writeJsonConfigFile(jsonConfigPath, fileContent); -} - -export async function removeLayout({ - projectDir = state.projectDir, - schemaName, - dataSourceName, - runCodegen = true, -}: { - projectDir?: string; - schemaName: string; - dataSourceName: string; - runCodegen?: boolean; -}) { - const jsonConfigPath = path.join(projectDir, typegenConfigFileName); - const fileContent = await readJsonConfigFile(jsonConfigPath); - - if (!fileContent) { - throw new Error(`${typegenConfigFileName} not found, cannot remove layout.`); - } - - let dataSourceModified = false; - const targetDsPath = `./src/config/schemas/${dataSourceName}`; - - let configArray: AnyDataSourceConfig[]; - if (Array.isArray(fileContent.config)) { - configArray = fileContent.config; - } else { - configArray = [fileContent.config]; - fileContent.config = configArray; - } - - const targetDataSource = configArray.find( - (ds): ds is FmdapiDataSourceConfig => ds.type === "fmdapi" && ds.path === targetDsPath, - ); - - if (targetDataSource?.layouts) { - const initialCount = targetDataSource.layouts.length; - targetDataSource.layouts = targetDataSource.layouts.filter((layout) => layout.schemaName !== schemaName); - if (targetDataSource.layouts.length < initialCount) { - dataSourceModified = true; - } - } - - if (dataSourceModified) { - await writeJsonConfigFile(jsonConfigPath, fileContent); - } - - const schemaFilePath = path.join(projectDir, "src", "config", "schemas", dataSourceName, `${schemaName}.ts`); - if (fs.existsSync(schemaFilePath)) { - fs.removeSync(schemaFilePath); - } - - if (runCodegen && dataSourceModified) { - await runCodegenCommand(); - } -} - -// Make sure to remove unused imports like Project, SyntaxKind, etc. if they are no longer used anywhere. -// Also remove getNewProject and formatAndSaveSourceFiles from imports if they were only for config. diff --git a/packages/cli/src/generators/route.ts b/packages/cli/src/generators/route.ts deleted file mode 100644 index e008a05c..00000000 --- a/packages/cli/src/generators/route.ts +++ /dev/null @@ -1,40 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; -import type { RouteLink } from "index.js"; -import { SyntaxKind } from "ts-morph"; - -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; - -export async function addRouteToNav({ - projectDir, - navType, - ...route -}: Omit & { - projectDir: string; - navType: "primary" | "secondary"; -}) { - const navFilePath = path.join(projectDir, "src/app/navigation.tsx"); - - // If the navigation file doesn't exist (e.g., Web Viewer apps), skip adding to nav - if (!fs.existsSync(navFilePath)) { - return; - } - - const project = getNewProject(projectDir); - const sourceFile = project.addSourceFileAtPath(navFilePath); - sourceFile - .getVariableDeclaration(navType === "primary" ? "primaryRoutes" : "secondaryRoutes") - ?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression) - ?.addElement((writer) => - writer - .block(() => { - writer.write(` - label: "${route.label}", - type: "link", - href: "${route.href}",`); - }) - .write(","), - ); - - await formatAndSaveSourceFiles(project); -} diff --git a/packages/cli/src/generators/tanstack-query.ts b/packages/cli/src/generators/tanstack-query.ts deleted file mode 100644 index 874eee0d..00000000 --- a/packages/cli/src/generators/tanstack-query.ts +++ /dev/null @@ -1,97 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; -import { type Project, SyntaxKind } from "ts-morph"; - -import { PKG_ROOT } from "~/consts.js"; -import { state } from "~/state.js"; -import { addPackageDependency } from "~/utils/addPackageDependency.js"; -import { getSettings, setSettings } from "~/utils/parseSettings.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; - -export async function injectTanstackQuery(args?: { project?: Project }) { - const projectDir = state.projectDir; - const settings = getSettings(); - if (settings.ui === "shadcn") { - return false; - } - if (settings.tanstackQuery) { - return false; - } - - addPackageDependency({ - projectDir, - dependencies: ["@tanstack/react-query"], - devMode: false, - }); - addPackageDependency({ - projectDir, - dependencies: ["@tanstack/react-query-devtools"], - devMode: true, - }); - const extrasDir = path.join(PKG_ROOT, "template", "extras"); - - if (state.appType === "browser") { - fs.copySync( - path.join(extrasDir, "config", "get-query-client.ts"), - path.join(projectDir, "src/config/get-query-client.ts"), - ); - fs.copySync( - path.join(extrasDir, "config", "query-provider.tsx"), - path.join(projectDir, "src/config/query-provider.tsx"), - ); - } else if (state.appType === "webviewer") { - fs.copySync( - path.join(extrasDir, "config", "query-provider-vite.tsx"), - path.join(projectDir, "src/config/query-provider.tsx"), - ); - } - - // inject query provider into the root layout - const project = args?.project ?? getNewProject(projectDir); - const rootLayout = project.addSourceFileAtPath( - path.join(projectDir, state.appType === "browser" ? "src/app/layout.tsx" : "src/main.tsx"), - ); - rootLayout.addImportDeclaration({ - moduleSpecifier: "@/config/query-provider", - defaultImport: "QueryProvider", - }); - - if (state.appType === "browser") { - const exportDefault = rootLayout.getFunction((dec) => dec.isDefaultExport()); - const bodyElement = exportDefault - ?.getBody() - ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement) - ?.getDescendantsOfKind(SyntaxKind.JsxOpeningElement) - .find((openingElement) => openingElement.getTagNameNode().getText() === "body") - ?.getParentIfKind(SyntaxKind.JsxElement); - - const childrenText = bodyElement - ?.getJsxChildren() - .map((child) => child.getText()) - .filter(Boolean) - .join("\n"); - - bodyElement?.getChildSyntaxList()?.replaceWithText( - ` - ${childrenText} - `, - ); - } else if (state.appType === "webviewer") { - const mantineProvider = rootLayout - .getDescendantsOfKind(SyntaxKind.JsxElement) - .find((element) => element.getOpeningElement().getTagNameNode().getText() === "MantineProvider"); - - mantineProvider?.replaceWithText( - ` - ${mantineProvider.getText()} - `, - ); - } - - if (!args?.project) { - await formatAndSaveSourceFiles(project); - } - - setSettings({ ...settings, tanstackQuery: true }); - return true; -} diff --git a/packages/cli/src/globalOptions.ts b/packages/cli/src/globalOptions.ts deleted file mode 100644 index 9435e511..00000000 --- a/packages/cli/src/globalOptions.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Option } from "commander"; - -export const nonInteractiveOption = new Option( - "--non-interactive", - "Never prompt for input; fail with a clear error when required values are missing", -).default(false); -export const debugOption = new Option("--debug", "Run in debug mode").default(false); diff --git a/packages/cli/src/helpers/createProject.ts b/packages/cli/src/helpers/createProject.ts deleted file mode 100644 index 71a8ca24..00000000 --- a/packages/cli/src/helpers/createProject.ts +++ /dev/null @@ -1,112 +0,0 @@ -import path from "node:path"; - -import { getAgentInstructions } from "~/consts.js"; -import { installPackages } from "~/helpers/installPackages.js"; -import { scaffoldProject } from "~/helpers/scaffoldProject.js"; -import type { AvailableDependencies } from "~/installers/dependencyVersionMap.js"; -import type { PkgInstallerMap } from "~/installers/index.js"; -import { state } from "~/state.js"; -import { addPackageDependency } from "~/utils/addPackageDependency.js"; -import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; -import { replaceTextInFiles } from "./replaceText.js"; - -interface CreateProjectOptions { - projectName: string; - packages: PkgInstallerMap; - scopedAppName: string; - noInstall: boolean; - force: boolean; - appRouter: boolean; -} - -export const createBareProject = async ({ - projectName, - scopedAppName, - packages, - noInstall, - force, -}: CreateProjectOptions) => { - const pkgManager = getUserPkgManager(); - state.projectDir = path.resolve(process.cwd(), projectName); - - // Bootstraps the base Next.js application - await scaffoldProject({ - projectName, - pkgManager, - scopedAppName, - noInstall, - force, - }); - - addPackageDependency({ - dependencies: ["@types/node"], - devMode: true, - }); - - // Add base deps for current templates. Legacy Mantine projects remain supported elsewhere. - const NEXT_SHADCN_BASE_DEPS = [ - "@radix-ui/react-slot", - "@tailwindcss/postcss", - "class-variance-authority", - "clsx", - "lucide-react", - "tailwind-merge", - "tailwindcss", - "tw-animate-css", - "next-themes", - ] as AvailableDependencies[]; - const VITE_SHADCN_BASE_DEPS = [ - "@radix-ui/react-slot", - "@tailwindcss/vite", - "@proofkit/fmdapi", - "@proofkit/webviewer", - "class-variance-authority", - "clsx", - "lucide-react", - "tailwind-merge", - "tailwindcss", - "tw-animate-css", - "zod", - ] as AvailableDependencies[]; - const SHADCN_BASE_DEV_DEPS = ["oxlint", "ultracite"] as AvailableDependencies[]; - const VITE_SHADCN_BASE_DEV_DEPS = ["@proofkit/typegen", "oxlint", "ultracite"] as AvailableDependencies[]; - - if (state.ui === "shadcn") { - addPackageDependency({ - dependencies: state.appType === "webviewer" ? VITE_SHADCN_BASE_DEPS : NEXT_SHADCN_BASE_DEPS, - devMode: false, - }); - addPackageDependency({ - dependencies: state.appType === "webviewer" ? VITE_SHADCN_BASE_DEV_DEPS : SHADCN_BASE_DEV_DEPS, - devMode: true, - }); - } else { - throw new Error(`Unsupported scaffold UI library: ${state.ui}`); - } - - // Install the selected packages - installPackages({ - projectName, - scopedAppName, - pkgManager, - packages, - noInstall, - }); - - let pkgManagerCommand: string; - if (pkgManager === "pnpm") { - pkgManagerCommand = "pnpm"; - } else if (pkgManager === "bun") { - pkgManagerCommand = "bun"; - } else if (pkgManager === "yarn") { - pkgManagerCommand = "yarn"; - } else { - pkgManagerCommand = "npm run"; - } - - replaceTextInFiles(state.projectDir, "__PNPM_COMMAND__", pkgManagerCommand); - replaceTextInFiles(state.projectDir, "__PACKAGE_MANAGER__", pkgManager); - replaceTextInFiles(state.projectDir, "__AGENT_INSTRUCTIONS__", getAgentInstructions()); - - return state.projectDir; -}; diff --git a/packages/cli/src/helpers/fmMcp.ts b/packages/cli/src/helpers/fmMcp.ts deleted file mode 100644 index ab58114e..00000000 --- a/packages/cli/src/helpers/fmMcp.ts +++ /dev/null @@ -1,56 +0,0 @@ -const defaultBaseUrl = process.env.FM_MCP_BASE_URL ?? "http://127.0.0.1:1365"; -const REQUEST_TIMEOUT_MS = 3000; - -export interface FmMcpStatus { - baseUrl: string; - healthy: boolean; - connectedFiles: string[]; -} - -async function fetchWithTimeout(url: string): Promise { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); - - try { - return await fetch(url, { signal: controller.signal }); - } catch { - return null; - } finally { - clearTimeout(timeoutId); - } -} - -async function readJson(url: string): Promise { - const response = await fetchWithTimeout(url); - - if (!response?.ok) { - return null; - } - - return await response.json().catch(() => null); -} - -export async function getFmMcpStatus(baseUrl = defaultBaseUrl): Promise { - const healthResponse = await fetchWithTimeout(`${baseUrl}/health`); - - if (!healthResponse?.ok) { - return { - baseUrl, - healthy: false, - connectedFiles: [], - }; - } - - const connectedFiles = await readJson(`${baseUrl}/connectedFiles`); - - return { - baseUrl, - healthy: true, - connectedFiles: Array.isArray(connectedFiles) ? connectedFiles : [], - }; -} - -export async function detectConnectedFmFile(baseUrl = defaultBaseUrl): Promise { - const status = await getFmMcpStatus(baseUrl); - return status.connectedFiles[0]; -} diff --git a/packages/cli/src/helpers/git.ts b/packages/cli/src/helpers/git.ts deleted file mode 100644 index bdeaefee..00000000 --- a/packages/cli/src/helpers/git.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { execSync } from "node:child_process"; -import path from "node:path"; -import chalk from "chalk"; -import { execa } from "execa"; -import fs from "fs-extra"; -import ora from "ora"; -import * as p from "~/cli/prompts.js"; - -import { isNonInteractiveMode } from "~/state.js"; -import { logger } from "~/utils/logger.js"; - -const isGitInstalled = (dir: string): boolean => { - try { - execSync("git --version", { cwd: dir }); - return true; - } catch (_e) { - return false; - } -}; - -/** @returns Whether or not the provided directory has a `.git` subdirectory in it. */ -export const isRootGitRepo = (dir: string): boolean => { - return fs.existsSync(path.join(dir, ".git")); -}; - -/** @returns Whether or not this directory or a parent directory has a `.git` directory. */ -export const isInsideGitRepo = async (dir: string): Promise => { - try { - // If this command succeeds, we're inside a git repo - await execa("git", ["rev-parse", "--is-inside-work-tree"], { - cwd: dir, - stdout: "ignore", - }); - return true; - } catch (_e) { - // Else, it will throw a git-error and we return false - return false; - } -}; - -const getGitVersion = () => { - const stdout = execSync("git --version").toString().trim(); - const gitVersionTag = stdout.split(" ")[2]; - const major = gitVersionTag?.split(".")[0]; - const minor = gitVersionTag?.split(".")[1]; - return { major: Number(major), minor: Number(minor) }; -}; - -/** @returns The git config value of "init.defaultBranch". If it is not set, returns "main". */ -const getDefaultBranch = () => { - const stdout = execSync("git config --global init.defaultBranch || echo main").toString().trim(); - - return stdout; -}; - -// This initializes the Git-repository for the project -export const initializeGit = async (projectDir: string) => { - logger.info("Initializing Git..."); - - if (!isGitInstalled(projectDir)) { - logger.warn("Git is not installed. Skipping Git initialization."); - return; - } - - const spinner = ora("Creating a new git repo...\n").start(); - - const isRoot = isRootGitRepo(projectDir); - const isInside = await isInsideGitRepo(projectDir); - const dirName = path.parse(projectDir).name; // skip full path for logging - - if (isInside && isRoot) { - // Dir is a root git repo - spinner.stop(); - if (isNonInteractiveMode()) { - throw new Error( - `Cannot initialize git in non-interactive mode because "${dirName}" already contains a git repository.`, - ); - } - const overwriteGit = await p.confirm({ - message: `${chalk.redBright.bold( - "Warning:", - )} Git is already initialized in "${dirName}". Initializing a new git repository would delete the previous history. Would you like to continue anyways?`, - initialValue: false, - }); - - if (!overwriteGit) { - spinner.info("Skipping Git initialization."); - return; - } - // Deleting the .git folder - fs.removeSync(path.join(projectDir, ".git")); - } else if (isInside && !isRoot) { - // Dir is inside a git worktree - spinner.stop(); - if (isNonInteractiveMode()) { - throw new Error( - `Cannot initialize git in non-interactive mode because "${dirName}" is already inside a git worktree.`, - ); - } - const initializeChildGitRepo = await p.confirm({ - message: `${chalk.redBright.bold( - "Warning:", - )} "${dirName}" is already in a git worktree. Would you still like to initialize a new git repository in this directory?`, - initialValue: false, - }); - if (!initializeChildGitRepo) { - spinner.info("Skipping Git initialization."); - return; - } - } - - // We're good to go, initializing the git repo - try { - const branchName = getDefaultBranch(); - - // --initial-branch flag was added in git v2.28.0 - const { major, minor } = getGitVersion(); - if (major < 2 || (major === 2 && minor < 28)) { - await execa("git", ["init"], { cwd: projectDir }); - // symbolic-ref is used here due to refs/heads/master not existing - // It is only created after the first commit - // https://superuser.com/a/1419674 - await execa("git", ["symbolic-ref", "HEAD", `refs/heads/${branchName}`], { - cwd: projectDir, - }); - } else { - await execa("git", ["init", `--initial-branch=${branchName}`], { - cwd: projectDir, - }); - } - await execa("git", ["add", "."], { cwd: projectDir }); - await execa("git", ["commit", "-m", "Initial commit"], { - cwd: projectDir, - }); - spinner.succeed(`${chalk.green("Successfully initialized and staged")} ${chalk.green.bold("git")}\n`); - } catch (_error) { - // Safeguard, should be unreachable - spinner.fail(`${chalk.bold.red("Failed:")} could not initialize git. Update git to the latest version!\n`); - } -}; diff --git a/packages/cli/src/helpers/installDependencies.ts b/packages/cli/src/helpers/installDependencies.ts deleted file mode 100644 index 880bd436..00000000 --- a/packages/cli/src/helpers/installDependencies.ts +++ /dev/null @@ -1,242 +0,0 @@ -import chalk from "chalk"; -import { execa, type StdoutStderrOption } from "execa"; -import ora, { type Ora } from "ora"; - -import { state } from "~/state.js"; -import { getUserPkgManager, type PackageManager } from "~/utils/getUserPkgManager.js"; -import { logger } from "~/utils/logger.js"; - -const execWithSpinner = async ( - projectDir: string, - pkgManager: PackageManager | "pnpx" | "bunx", - options: { - args?: string[]; - stdout?: StdoutStderrOption; - onDataHandle?: (spinner: Ora) => (data: Buffer) => void; - loadingMessage?: string; - }, -) => { - const { onDataHandle, args = ["install"], stdout = "pipe" } = options; - - if (process.env.PROOFKIT_ENV === "development") { - args.push("--prefer-offline"); - } - - const spinner = ora(options.loadingMessage ?? `Running ${pkgManager} ${args.join(" ")} ...`).start(); - const subprocess = execa(pkgManager, args, { - cwd: projectDir, - stdout, - stderr: "pipe", // Capture stderr to get error messages - }); - - await new Promise((res, rej) => { - let stdoutOutput = ""; - let stderrOutput = ""; - - if (onDataHandle) { - subprocess.stdout?.on("data", onDataHandle(spinner)); - } else { - // If no custom handler, capture stdout for error reporting - subprocess.stdout?.on("data", (data) => { - stdoutOutput += data.toString(); - }); - } - - // Capture stderr output for error reporting - subprocess.stderr?.on("data", (data) => { - stderrOutput += data.toString(); - }); - - subprocess.on("error", (e) => rej(e)); - subprocess.on("close", (code) => { - if (code === 0) { - res(); - } else { - // Combine stdout and stderr for complete error message - const combinedOutput = [stdoutOutput, stderrOutput] - .filter((output) => output.trim()) - .join("\n") - .trim() - // Remove spinner-related lines that aren't useful in error output - .replace(/^- Checking registry\.$/gm, "") - .replace(/^\s*$/gm, "") // Remove empty lines - .trim(); - - const errorMessage = combinedOutput || `Command failed with exit code ${code}: ${pkgManager} ${args.join(" ")}`; - rej(new Error(errorMessage)); - } - }); - }); - - return spinner; -}; - -const runInstallCommand = async (pkgManager: PackageManager, projectDir: string): Promise => { - switch (pkgManager) { - // When using npm, inherit the stderr stream so that the progress bar is shown - case "npm": - await execa(pkgManager, ["install"], { - cwd: projectDir, - stderr: "inherit", - }); - - return null; - // When using yarn or pnpm, use the stdout stream and ora spinner to show the progress - case "pnpm": - return execWithSpinner(projectDir, pkgManager, { - onDataHandle: (spinner) => (data) => { - const text = data.toString(); - - if (text.includes("Progress")) { - spinner.text = text.includes("|") ? (text.split(" | ")[1] ?? "") : text; - } - }, - }); - case "yarn": - return execWithSpinner(projectDir, pkgManager, { - onDataHandle: (spinner) => (data) => { - spinner.text = data.toString(); - }, - }); - // When using bun, the stdout stream is ignored and the spinner is shown - case "bun": - return execWithSpinner(projectDir, pkgManager, { stdout: "ignore" }); - default: - throw new Error(`Unknown package manager: ${pkgManager}`); - } -}; - -export const installDependencies = async (args?: { projectDir?: string }) => { - const { projectDir = state.projectDir } = args ?? {}; - logger.info("Installing dependencies..."); - const pkgManager = getUserPkgManager(); - - const installSpinner = await runInstallCommand(pkgManager, projectDir); - - // If the spinner was used to show the progress, use succeed method on it - // If not, use the succeed on a new spinner - (installSpinner ?? ora()).succeed(chalk.green("Successfully installed dependencies!\n")); -}; - -export const runExecCommand = async ({ - command, - projectDir = state.projectDir, - successMessage, - errorMessage, - loadingMessage, -}: { - command: string[]; - projectDir?: string; - successMessage?: string; - errorMessage?: string; - loadingMessage?: string; -}) => { - let spinner: Ora | null = null; - - try { - spinner = await _runExecCommand({ - projectDir, - command, - loadingMessage, - }); - - // If the spinner was used to show the progress, use succeed method on it - // If not, use the succeed on a new spinner - (spinner ?? ora()).succeed( - chalk.green(successMessage ? `${successMessage}\n` : `Successfully ran ${command.join(" ")}!\n`), - ); - } catch (error) { - // If we have a spinner, fail it, otherwise just throw the error - if (spinner) { - const failMessage = errorMessage || `Failed to run ${command.join(" ")}`; - spinner.fail(chalk.red(failMessage)); - } - throw error; - } -}; - -export const _runExecCommand = async ({ - projectDir, - command, - loadingMessage, -}: { - projectDir: string; - exec?: boolean; - command: string[]; - loadingMessage?: string; -}): Promise => { - const pkgManager = getUserPkgManager(); - switch (pkgManager) { - // When using npm, capture both stdout and stderr to show error messages - case "npm": { - const result = await execa("npx", [...command], { - cwd: projectDir, - stdout: "pipe", - stderr: "pipe", - reject: false, - }); - - if (result.exitCode !== 0) { - // Combine stdout and stderr for complete error message - const combinedOutput = [result.stdout, result.stderr] - .filter((output) => output?.trim()) - .join("\n") - .trim() - // Remove spinner-related lines that aren't useful in error output - .replace(/^- Checking registry\.$/gm, "") - .replace(/^\s*$/gm, "") // Remove empty lines - .trim(); - - const errorMessage = - combinedOutput || `Command failed with exit code ${result.exitCode}: npx ${command.join(" ")}`; - throw new Error(errorMessage); - } - - return null; - } - // When using yarn or pnpm, use the stdout stream and ora spinner to show the progress - case "pnpm": { - // For shadcn commands, don't use progress handler to capture full output - const isInstallCommand = command.includes("install"); - return execWithSpinner(projectDir, "pnpm", { - args: ["dlx", ...command], - loadingMessage, - onDataHandle: isInstallCommand - ? (spinner) => (data) => { - const text = data.toString(); - - if (text.includes("Progress")) { - spinner.text = text.includes("|") ? (text.split(" | ")[1] ?? "") : text; - } - } - : undefined, - }); - } - case "yarn": { - // For shadcn commands, don't use progress handler to capture full output - const isYarnInstallCommand = command.includes("install"); - return execWithSpinner(projectDir, pkgManager, { - args: [...command], - loadingMessage, - onDataHandle: isYarnInstallCommand - ? (spinner) => (data) => { - spinner.text = data.toString(); - } - : undefined, - }); - } - // When using bun, the stdout stream is ignored and the spinner is shown - case "bun": - return execWithSpinner(projectDir, "bunx", { - stdout: "ignore", - args: [...command], - loadingMessage, - }); - default: - throw new Error(`Unknown package manager: ${pkgManager}`); - } -}; - -export function generateRandomSecret(): string { - return crypto.randomUUID().replace(/-/g, ""); -} diff --git a/packages/cli/src/helpers/installPackages.ts b/packages/cli/src/helpers/installPackages.ts deleted file mode 100644 index 06345c47..00000000 --- a/packages/cli/src/helpers/installPackages.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { InstallerOptions, PkgInstallerMap } from "~/installers/index.js"; -import { logger } from "~/utils/logger.js"; - -type InstallPackagesOptions = InstallerOptions & { - packages: PkgInstallerMap; -}; -// This runs the installer for all the packages that the user has selected -export const installPackages = (options: InstallPackagesOptions) => { - const { packages } = options; - logger.info("Adding boilerplate..."); - - for (const [_name, pkgOpts] of Object.entries(packages)) { - if (pkgOpts.inUse) { - // const spinner = ora(`Boilerplating ${name}...`).start(); - pkgOpts.installer(options); - // spinner.succeed( - // chalk.green( - // `Successfully setup boilerplate for ${chalk.green.bold(name)}` - // ) - // ); - } - } - - logger.info(""); -}; diff --git a/packages/cli/src/helpers/intent.ts b/packages/cli/src/helpers/intent.ts deleted file mode 100644 index 83c6a139..00000000 --- a/packages/cli/src/helpers/intent.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { PackageManager } from "~/utils/packageManager.js"; -import { getTemplatePackageExecuteCommand, parseCommandString } from "~/utils/projectFiles.js"; - -function splitExecuteCommand(packageManager: PackageManager) { - const [command, ...args] = parseCommandString(getTemplatePackageExecuteCommand(packageManager)); - if (!command) { - throw new Error(`Unable to resolve package execute command for ${packageManager}.`); - } - return { command, args }; -} - -export function getIntentInstallCommand(packageManager: PackageManager) { - const execute = splitExecuteCommand(packageManager); - return { - command: execute.command, - args: [...execute.args, "@tanstack/intent@latest", "install"], - }; -} diff --git a/packages/cli/src/helpers/logNextSteps.ts b/packages/cli/src/helpers/logNextSteps.ts deleted file mode 100644 index c8ae3b11..00000000 --- a/packages/cli/src/helpers/logNextSteps.ts +++ /dev/null @@ -1,44 +0,0 @@ -import chalk from "chalk"; - -import { DEFAULT_APP_NAME } from "~/consts.js"; -import type { InstallerOptions } from "~/installers/index.js"; -import { state } from "~/state.js"; -import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; -import { logger } from "~/utils/logger.js"; - -const formatRunCommand = (pkgManager: ReturnType, command: string) => - ["npm", "bun"].includes(pkgManager) ? `${pkgManager} run ${command}` : `${pkgManager} ${command}`; - -// This logs the next steps that the user should take in order to advance the project -export const logNextSteps = ({ - projectName = DEFAULT_APP_NAME, - noInstall, -}: Pick) => { - const pkgManager = getUserPkgManager(); - - logger.info(chalk.bold("Next steps:")); - logger.dim("\nNavigate to the project directory:"); - projectName !== "." && logger.info(` cd ${projectName}`); - logger.dim("(or open in your code editor, and run the rest of these commands from there)"); - - if (noInstall) { - logger.dim("\nInstall dependencies:"); - // To reflect yarn's default behavior of installing packages when no additional args provided - if (pkgManager === "yarn") { - logger.info(` ${pkgManager}`); - } else { - logger.info(` ${pkgManager} install`); - } - } - - logger.dim("\nStart the dev server to view your app in a browser:"); - logger.info(` ${formatRunCommand(pkgManager, "dev")}`); - - if (state.appType === "webviewer") { - logger.dim("\nWhen you're ready to generate FileMaker clients:"); - logger.info(` ${formatRunCommand(pkgManager, "typegen")}`); - - logger.dim("\nTo open the starter inside FileMaker once your file is ready:"); - logger.info(` ${formatRunCommand(pkgManager, "launch-fm")}`); - } -}; diff --git a/packages/cli/src/helpers/replaceText.ts b/packages/cli/src/helpers/replaceText.ts deleted file mode 100644 index e7f9d4b1..00000000 --- a/packages/cli/src/helpers/replaceText.ts +++ /dev/null @@ -1,17 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; - -export function replaceTextInFiles(directoryPath: string, search: string, replacement: string): void { - const files = fs.readdirSync(directoryPath); - - for (const file of files) { - const filePath = path.join(directoryPath, file); - if (fs.statSync(filePath).isDirectory()) { - replaceTextInFiles(filePath, search, replacement); - } else { - const data = fs.readFileSync(filePath, "utf8"); - const updatedData = data.replace(new RegExp(search, "g"), replacement); - fs.writeFileSync(filePath, updatedData, "utf8"); - } - } -} diff --git a/packages/cli/src/helpers/scaffoldProject.ts b/packages/cli/src/helpers/scaffoldProject.ts deleted file mode 100644 index f5667760..00000000 --- a/packages/cli/src/helpers/scaffoldProject.ts +++ /dev/null @@ -1,132 +0,0 @@ -import path from "node:path"; -import chalk from "chalk"; -import fs from "fs-extra"; -import ora from "ora"; -import * as p from "~/cli/prompts.js"; - -import { PKG_ROOT } from "~/consts.js"; -import type { InstallerOptions } from "~/installers/index.js"; -import { isNonInteractiveMode, state } from "~/state.js"; -import { logger } from "~/utils/logger.js"; - -const AGENT_METADATA_DIRS = new Set([".agents", ".claude", ".clawed", ".clinerules", ".cursor", ".windsurf"]); - -function getMeaningfulDirectoryEntries(projectDir: string): string[] { - return fs.readdirSync(projectDir).filter((entry) => { - if (AGENT_METADATA_DIRS.has(entry)) { - return false; - } - - if (entry === ".gitignore") { - return true; - } - - if (entry.startsWith(".")) { - return false; - } - - return true; - }); -} - -// This bootstraps the base Next.js application -export const scaffoldProject = async ({ - projectName, - pkgManager, - noInstall, - force = false, -}: InstallerOptions & { force?: boolean }) => { - const projectDir = state.projectDir; - - const srcDir = path.join(PKG_ROOT, state.appType === "browser" ? "template/nextjs-shadcn" : "template/vite-wv"); - - if (noInstall) { - logger.info(""); - } else { - logger.info(`\nUsing: ${chalk.cyan.bold(pkgManager)}\n`); - } - - const spinner = ora(`Scaffolding in: ${projectDir}...\n`).start(); - - if (fs.existsSync(projectDir)) { - const meaningfulEntries = getMeaningfulDirectoryEntries(projectDir); - - if (meaningfulEntries.length === 0) { - if (projectName !== ".") { - spinner.info(`${chalk.cyan.bold(projectName)} exists but is empty, continuing...\n`); - } - } else if (force) { - spinner.info( - `${chalk.yellow("Force mode enabled:")} clearing ${chalk.cyan.bold(projectName)} before scaffolding...\n`, - ); - fs.emptyDirSync(projectDir); - spinner.start(); - // continue to scaffold after clearing - } else if (isNonInteractiveMode()) { - spinner.fail( - `${chalk.redBright.bold("Error:")} ${chalk.cyan.bold( - projectName, - )} already exists and isn't empty. Remove the existing files or choose a different directory.`, - ); - throw new Error( - `Cannot initialize into a non-empty directory in non-interactive mode: ${meaningfulEntries.join(", ")}`, - ); - } else { - spinner.stopAndPersist(); - const overwriteDir = await p.select({ - message: `${chalk.redBright.bold("Warning:")} ${chalk.cyan.bold( - projectName, - )} already exists and isn't empty. How would you like to proceed?`, - options: [ - { - label: "Abort installation (recommended)", - value: "abort", - }, - { - label: "Clear the directory and continue installation", - value: "clear", - }, - { - label: "Continue installation and overwrite conflicting files", - value: "overwrite", - }, - ], - initialValue: "abort", - }); - if (overwriteDir === "abort") { - spinner.fail("Aborting installation..."); - process.exit(1); - } - - const overwriteAction = overwriteDir === "clear" ? "clear the directory" : "overwrite conflicting files"; - - const confirmOverwriteDir = await p.confirm({ - message: `Are you sure you want to ${overwriteAction}?`, - initialValue: false, - }); - - if (!confirmOverwriteDir) { - spinner.fail("Aborting installation..."); - process.exit(1); - } - - if (overwriteDir === "clear") { - spinner.info(`Emptying ${chalk.cyan.bold(projectName)} and creating new ProofKit app..\n`); - fs.emptyDirSync(projectDir); - } - } - } - - spinner.start(); - - // Copy the main template - fs.copySync(srcDir, projectDir); - - // Rename gitignore - fs.renameSync(path.join(projectDir, "_gitignore"), path.join(projectDir, ".gitignore")); - fs.writeFileSync(path.join(projectDir, ".cursorignore"), "CLAUDE.md\n", "utf8"); - - const scaffoldedName = projectName === "." ? "App" : chalk.cyan.bold(projectName); - - spinner.succeed(`${scaffoldedName} ${chalk.green("scaffolded successfully!")}\n`); -}; diff --git a/packages/cli/src/helpers/selectBoilerplate.ts b/packages/cli/src/helpers/selectBoilerplate.ts deleted file mode 100644 index 4b538d3d..00000000 --- a/packages/cli/src/helpers/selectBoilerplate.ts +++ /dev/null @@ -1,32 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; - -import { PKG_ROOT } from "~/consts.js"; -import type { InstallerOptions } from "~/installers/index.js"; -import { state } from "~/state.js"; - -type SelectBoilerplateProps = Required>; - -export const selectLayoutFile = (_props: SelectBoilerplateProps) => { - const projectDir = state.projectDir; - const layoutFileDir = path.join(PKG_ROOT, "template/extras/src/app/layout"); - - const layoutFile = "base.tsx"; - - const appSrc = path.join(layoutFileDir, layoutFile); // base layout - const appDest = path.join(projectDir, "src/app/layout.tsx"); - fs.copySync(appSrc, appDest); - - fs.copySync(path.join(layoutFileDir, "main-shell.tsx"), path.join(projectDir, "src/app/(main)/layout.tsx")); -}; - -export const selectPageFile = (_props: SelectBoilerplateProps) => { - const projectDir = state.projectDir; - const indexFileDir = path.join(PKG_ROOT, "template/extras/src/app/page"); - - const indexFile = "base.tsx"; - - const indexSrc = path.join(indexFileDir, indexFile); - const indexDest = path.join(projectDir, "src/app/(main)/page.tsx"); - fs.copySync(indexSrc, indexDest); -}; diff --git a/packages/cli/src/helpers/setImportAlias.ts b/packages/cli/src/helpers/setImportAlias.ts deleted file mode 100644 index 7551134b..00000000 --- a/packages/cli/src/helpers/setImportAlias.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { replaceTextInFiles } from "./replaceText.js"; - -const TRAILING_SLASH_REGEX = /[^/]$/; - -export const setImportAlias = (projectDir: string, importAlias: string) => { - const normalizedImportAlias = importAlias - .replace(/\*/g, "") // remove any wildcards (~/* -> ~/) - .replace(TRAILING_SLASH_REGEX, "$&/"); // ensure trailing slash (@ -> ~/) - - // update import alias in any files if not using the default - replaceTextInFiles(projectDir, "~/", normalizedImportAlias); -}; diff --git a/packages/cli/src/helpers/stealth-init.ts b/packages/cli/src/helpers/stealth-init.ts deleted file mode 100644 index 6f865ab8..00000000 --- a/packages/cli/src/helpers/stealth-init.ts +++ /dev/null @@ -1,20 +0,0 @@ -import fs from "fs-extra"; - -import { defaultSettings, setSettings, validateAndSetEnvFile } from "~/utils/parseSettings.js"; - -/** - * Used to add a proofkit.json file to an existing project - */ -export async function stealthInit() { - // check if proofkit.json exists - const proofkitJson = await fs.pathExists("proofkit.json"); - if (proofkitJson) { - return; - } - - // create proofkit.json with default settings - setSettings(defaultSettings); - - // validate and set envFile only if it exists - validateAndSetEnvFile(); -} diff --git a/packages/cli/src/helpers/ultracite.ts b/packages/cli/src/helpers/ultracite.ts deleted file mode 100644 index 463bbd71..00000000 --- a/packages/cli/src/helpers/ultracite.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { AppType } from "~/core/types.js"; -import type { PackageManager } from "~/utils/packageManager.js"; -import { getTemplatePackageExecuteCommand, parseCommandString } from "~/utils/projectFiles.js"; - -const ULTRACITE_EDITORS = ["cursor"] as const; -const ULTRACITE_AGENTS = ["claude", "codex"] as const; -const ULTRACITE_HOOKS = ["cursor", "windsurf"] as const; -const ULTRACITE_INIT_PACKAGE = "ultracite@^7"; - -function splitExecuteCommand(packageManager: PackageManager) { - const [command, ...args] = parseCommandString(getTemplatePackageExecuteCommand(packageManager)); - if (!command) { - throw new Error(`Unable to resolve package execute command for ${packageManager}.`); - } - return { command, args }; -} - -export function getUltraciteFrameworks(appType: AppType) { - return appType === "browser" ? ["react", "next"] : ["react"]; -} - -export function getUltraciteInitCommand({ - appType, - packageManager, - skipInstall, -}: { - appType: AppType; - packageManager: PackageManager; - skipInstall: boolean; -}) { - const execute = splitExecuteCommand(packageManager); - return { - command: execute.command, - args: [ - ...execute.args, - ULTRACITE_INIT_PACKAGE, - "init", - "--quiet", - "--linter", - "oxlint", - "--pm", - packageManager, - "--frameworks", - ...getUltraciteFrameworks(appType), - "--editors", - ...ULTRACITE_EDITORS, - "--agents", - ...ULTRACITE_AGENTS, - "--hooks", - ...ULTRACITE_HOOKS, - ...(skipInstall ? ["--skip-install"] : []), - ], - }; -} - -export function getBrowserOxlintConfig() { - return `import { defineConfig } from "oxlint"; -import core from "ultracite/oxlint/core"; -import next from "ultracite/oxlint/next"; -import react from "ultracite/oxlint/react"; - -export default defineConfig({ -\textends: [core, react, next], -\trules: { -\t\t"func-style": "off", -\t\t"next/no-img-element": "off", -\t\t"promise/prefer-await-to-then": "off", -\t\t"promise/prefer-catch": "off", -\t\t"unicorn/filename-case": "off", -\t}, -}); -`; -} - -export function getWebViewerOxlintConfig() { - return `import { defineConfig } from "oxlint"; -import core from "ultracite/oxlint/core"; -import react from "ultracite/oxlint/react"; - -export default defineConfig({ -\textends: [core, react], -\trules: { -\t\t"react/react-in-jsx-scope": "off", -\t}, -}); -`; -} - -export function getHuskyPreCommitHook() { - return `#!/bin/sh -echo "Running lint-staged..." -pnpm exec lint-staged -`; -} diff --git a/packages/cli/src/helpers/version-fetcher.ts b/packages/cli/src/helpers/version-fetcher.ts deleted file mode 100644 index 26a21e80..00000000 --- a/packages/cli/src/helpers/version-fetcher.ts +++ /dev/null @@ -1,131 +0,0 @@ -import https from "node:https"; -import { TRPCError } from "@trpc/server"; -import axios from "axios"; -import z from "zod/v4"; - -export async function fetchServerVersions({ url, ottoPort = 3030 }: { url: string; ottoPort?: number }) { - const fmsInfo = await fetchFMSVersionInfo(url); - const ottoInfo = await fetchOttoVersion({ url, ottoPort }); - return { fmsInfo, ottoInfo }; -} - -const fmsInfoSchema = z.object({ - data: z.object({ - APIVersion: z.number().optional(), - AcceptEARPassword: z.boolean().optional(), - AcceptEncrypted: z.boolean().optional(), - AcceptUnencrypted: z.boolean().optional(), - AdminLocalAuth: z.string().optional(), - AllowChangeUploadDBFolder: z.boolean().optional(), - AutoOpenForUpload: z.boolean().optional(), - DenyGuestAndAutoLogin: z.string().optional(), - Hostname: z.string().optional(), - IsAppleInternal: z.boolean().optional(), - IsETS: z.boolean().optional(), - PremisesType: z.string().optional(), - ProductVersion: z.string().optional(), - PublicKey: z.string().optional(), - RequiresDBPasswords: z.boolean().optional(), - ServerID: z.string().optional(), - ServerVersion: z.string(), - }), - result: z.number(), -}); - -export async function fetchFMSVersionInfo(url: string) { - const fmsUrl = new URL(url); - fmsUrl.pathname = "/fmws/serverinfo"; - - const fmsInfoResult = await fetchWithoutSSL(fmsUrl.toString()).then((r) => fmsInfoSchema.safeParse(r.data)); - if (!fmsInfoResult.success) { - console.error("fmsInfoResult.error", fmsInfoResult.error.issues); - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Invalid FileMaker Server URL", - }); - } - return fmsInfoResult.data.data; -} - -const ottoInfoSchema = z.object({ - Otto: z.object({ - version: z.string(), - serverNickname: z.string().default(""), - isLicenseValid: z.boolean().optional(), - }), - migratorVersion: z.string().optional(), - FileMakerServer: z.object({ - version: z.object({ - long: z.string(), - short: z.string(), - }), - running: z.boolean().optional(), - }), - isMac: z.boolean().optional(), - platform: z.string().optional(), - host: z.string().optional(), -}); - -const ottoInfoResponseSchema = z.object({ - response: ottoInfoSchema, -}); - -export async function fetchOttoVersion({ - url, - ottoPort = 3030, -}: { - url: string; - ottoPort?: number | null; -}): Promise | null> { - let ottoInfo = await fetchOtto4Version(url); - if (!ottoInfo) { - ottoInfo = await fetchOtto3Version(url, ottoPort); - } - return ottoInfo; -} - -async function fetchOtto4Version(url: string) { - try { - const otto4Url = new URL(url); - otto4Url.pathname = "/otto/api/info"; - const otto4Info = await fetchWithoutSSL(otto4Url.toString()).then((r) => { - return ottoInfoResponseSchema.parse(r.data).response; - }); - return otto4Info; - } catch (_error) { - console.log("unable to fetch otto4 info, trying otto3"); - return null; - } -} - -async function fetchOtto3Version(url: string, ottoPort: number | null) { - try { - const otto3Url = new URL(url); - otto3Url.port = ottoPort ? ottoPort.toString() : "3030"; - otto3Url.pathname = "/api/otto/info"; - const ottoInfo = await fetchWithoutSSL(otto3Url.toString()).then((res) => { - return ottoInfoSchema.parse(res.data); - }); - return ottoInfo; - } catch (error) { - if (error instanceof Error) { - console.error("otto3 fetch error", error.message); - } - return null; - } -} - -async function fetchWithoutSSL(url: string) { - const agent = new https.Agent({ - rejectUnauthorized: false, - }); - - const result = await axios.get(url, { - validateStatus: null, - headers: { Connection: "close" }, - httpsAgent: agent, - timeout: 10_000, - }); - - return result; -} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts deleted file mode 100644 index 3e669747..00000000 --- a/packages/cli/src/index.ts +++ /dev/null @@ -1,662 +0,0 @@ -#!/usr/bin/env node -import { realpathSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { optional as optionalArg, text as textArg, withDescription as withArgDescription } from "@effect/cli/Args"; -import { - make as makeCommand, - run, - withDescription as withCommandDescription, - withSubcommands, -} from "@effect/cli/Command"; -import { - boolean as booleanOption, - choice as choiceOption, - optional as optionalOption, - text as textOption, - withAlias, - withDescription as withOptionDescription, -} from "@effect/cli/Options"; -import { isValidationError } from "@effect/cli/ValidationError"; -import { layer as nodeContextLayer } from "@effect/platform-node/NodeContext"; -import { Cause, Effect, Exit } from "effect"; -import { getOrUndefined } from "effect/Option"; -import { cliName } from "~/consts.js"; -import { - CliContext, - ConsoleService, - FileSystemService, - PackageManagerService, - PromptService, - TemplateService, -} from "~/core/context.js"; -import { runDoctor } from "~/core/doctor.js"; -import { getCliErrorMessage, isCliError, NonInteractiveInputError, UserCancelledError } from "~/core/errors.js"; -import { executeInitPlan } from "~/core/executeInitPlan.js"; -import { planInit } from "~/core/planInit.js"; -import { runPrompt } from "~/core/prompt.js"; -import { resolveInitRequest } from "~/core/resolveInitRequest.js"; -import type { CliFlags } from "~/core/types.js"; -import { CLI_VERSION } from "~/package-versions.js"; -import { makeLiveLayer } from "~/services/live.js"; -import { resolveNonInteractiveMode } from "~/utils/nonInteractive.js"; -import { intro } from "~/utils/prompts.js"; -import { proofGradient, renderTitle } from "~/utils/renderTitle.js"; - -const defaultCliFlags: CliFlags = { - noGit: false, - noInstall: false, - force: false, - default: false, - CI: false, - importAlias: "~/", -}; - -function getCliVersion() { - return CLI_VERSION; -} - -export const runInit = (name?: string, rawFlags?: Partial) => - Effect.gen(function* () { - const templateService = yield* TemplateService; - const packageManagerService = yield* PackageManagerService; - const request = yield* resolveInitRequest(name, { - ...defaultCliFlags, - ...rawFlags, - }); - const templateDir = templateService.getTemplateDir(request.appType, request.ui); - const packageManagerVersionResult = request.noInstall - ? yield* Effect.either(packageManagerService.getVersion(request.packageManager, request.cwd)) - : yield* packageManagerService.getVersion(request.packageManager, request.cwd).pipe( - Effect.map((version) => ({ - _tag: "Right" as const, - right: version, - })), - ); - const packageManagerVersion = - packageManagerVersionResult._tag === "Right" ? packageManagerVersionResult.right : undefined; - const plan = planInit(request, { templateDir, packageManagerVersion }); - yield* executeInitPlan(plan); - return { request, plan }; - }); - -type ProjectMenuChoice = "add" | "remove" | "typegen" | "deploy" | "upgrade" | "doctor" | "prompt" | "docs"; - -function isPromptCancellationError(error: unknown) { - return error instanceof UserCancelledError || (error instanceof Error && error.name === "ExitPromptError"); -} - -function toProjectMenuCommandError(command: string, cause: unknown) { - if (isCliError(cause)) { - return cause; - } - - const error = new Error(`Failed to run \`${command}\` from project menu.`); - Object.assign(error, { cause }); - return error; -} - -const runProjectMenu = Effect.gen(function* () { - const prompt = yield* PromptService; - const consoleService = yield* ConsoleService; - - const menuChoice = yield* Effect.tryPromise({ - try: () => - prompt.select({ - message: "What would you like to do?", - options: [ - { - label: "Add Components", - value: "add", - hint: "Add new pages, schemas, data sources, etc.", - }, - { - label: "Remove Components", - value: "remove", - hint: "Remove pages, schemas, data sources, etc.", - }, - { - label: "Generate Types", - value: "typegen", - hint: "Update field definitions from your data sources", - }, - { - label: "Deploy", - value: "deploy", - hint: "Deploy your app to Vercel", - }, - { - label: "Upgrade Components", - value: "upgrade", - hint: "Update ProofKit components to latest version", - }, - { - label: "Doctor", - value: "doctor", - hint: "Inspect project health and next steps", - }, - { - label: "Prompt", - value: "prompt", - hint: "Show agent workflow guidance", - }, - { - label: "View Documentation", - value: "docs", - hint: "Open ProofKit documentation", - }, - ], - }), - catch: (cause) => - isPromptCancellationError(cause) - ? new UserCancelledError({ message: "User aborted the operation" }) - : toProjectMenuCommandError("menu selection", cause), - }); - - switch (menuChoice) { - case "doctor": - return yield* runDoctor; - case "prompt": - return yield* runPrompt; - case "docs": { - const { DOCS_URL } = yield* Effect.promise(() => import("~/consts.js")); - consoleService.info(`Opening ${DOCS_URL} in your browser...`); - const { default: open } = yield* Effect.promise(() => import("open")); - yield* Effect.promise(() => open(DOCS_URL)); - return; - } - case "add": - return yield* Effect.tryPromise({ - try: async () => { - const [{ runAdd }, { initProgramState, state }] = await Promise.all([ - import("~/cli/add/index.js"), - import("~/state.js"), - ]); - initProgramState({}); - state.baseCommand = "add"; - state.projectDir = process.cwd(); - await runAdd(undefined); - }, - catch: (cause) => toProjectMenuCommandError("add", cause), - }); - case "remove": - return yield* Effect.tryPromise({ - try: async () => { - const [{ runRemove }, { initProgramState, state }] = await Promise.all([ - import("~/cli/remove/index.js"), - import("~/state.js"), - ]); - initProgramState({}); - state.baseCommand = "remove"; - state.projectDir = process.cwd(); - await runRemove(undefined); - }, - catch: (cause) => toProjectMenuCommandError("remove", cause), - }); - case "typegen": - return yield* Effect.promise(async () => { - const [{ runTypegen }, { getSettings }, { state }] = await Promise.all([ - import("~/cli/typegen/index.js"), - import("~/utils/parseSettings.js"), - import("~/state.js"), - ]); - state.projectDir = process.cwd(); - await runTypegen({ settings: getSettings() }); - }); - case "deploy": - return yield* Effect.promise(async () => { - const [{ runDeploy }, { initProgramState, state }] = await Promise.all([ - import("~/cli/deploy/index.js"), - import("~/state.js"), - ]); - initProgramState({}); - state.baseCommand = "deploy"; - state.projectDir = process.cwd(); - await runDeploy(); - }); - case "upgrade": - return yield* Effect.promise(async () => { - const [{ runUpgrade }, { initProgramState, state }] = await Promise.all([ - import("~/cli/update/index.js"), - import("~/state.js"), - ]); - initProgramState({}); - state.baseCommand = "upgrade"; - state.projectDir = process.cwd(); - await runUpgrade(); - }); - default: - throw new Error(`Unknown menu choice: ${menuChoice}`); - } -}); - -export const runDefaultCommand = (rawFlags?: Partial) => - Effect.gen(function* () { - const cliContext = yield* CliContext; - const fsService = yield* FileSystemService; - const consoleService = yield* ConsoleService; - const flags = { ...defaultCliFlags, ...rawFlags }; - const settingsPath = path.join(cliContext.cwd, "proofkit.json"); - const hasProofKitProject = yield* fsService.exists(settingsPath); - - if (hasProofKitProject) { - intro(`Found ${proofGradient("ProofKit")} project`); - if (!(cliContext.nonInteractive || flags.CI || flags.nonInteractive)) { - return yield* runProjectMenu; - } - - consoleService.note( - [ - "ProofKit now focuses on project bootstrap, diagnostics, and agent entrypoints.", - "Use an explicit command such as `proofkit doctor`, `proofkit prompt`, or `proofkit init`.", - ].join("\n"), - "Project commands", - ); - return; - } - - if (cliContext.nonInteractive || flags.CI || flags.nonInteractive) { - return yield* Effect.fail( - new NonInteractiveInputError({ - message: - "The default command is interactive-only in non-interactive mode. Run an explicit command such as `proofkit init --non-interactive`.", - }), - ); - } - - intro(`No ${proofGradient("ProofKit")} project found, running \`init\``); - yield* runInit(undefined, { - ...flags, - default: true, - }); - }); - -const initDirectoryArg = optionalArg(textArg({ name: "dir" })).pipe( - withArgDescription("The project name or target directory. Use `.` for the current directory, best when it is empty."), -); - -function optionalTextOption(name: string, description: string) { - return optionalOption(textOption(name).pipe(withOptionDescription(description))); -} - -function optionalChoiceOption(name: string, choices: Choices, description: string) { - return optionalOption(choiceOption(name, choices).pipe(withOptionDescription(description))); -} - -function getCurrentTTYState() { - return { - stdinIsTTY: process.stdin?.isTTY, - stdoutIsTTY: process.stdout?.isTTY, - }; -} - -function legacyEffect(runLegacy: () => Promise, options?: { nonInteractive?: boolean; debug?: boolean }) { - const nonInteractive = resolveNonInteractiveMode({ - nonInteractive: options?.nonInteractive, - ...getCurrentTTYState(), - }); - - return makeLiveLayer({ - cwd: process.cwd(), - debug: options?.debug === true, - nonInteractive, - })(Effect.promise(runLegacy)); -} - -function makeInitCommand() { - return makeCommand( - "init", - { - dir: initDirectoryArg, - appType: optionalChoiceOption("app-type", ["browser", "webviewer"] as const, "The type of app to create"), - server: optionalTextOption("server", "The URL of your FileMaker Server"), - adminApiKey: optionalTextOption("admin-api-key", "Admin API key for OttoFMS"), - fileName: optionalTextOption( - "file-name", - "The FileMaker file name to use, including selecting a local connected file", - ), - layoutName: optionalTextOption("layout-name", "The FileMaker layout name to scaffold"), - schemaName: optionalTextOption("schema-name", "The generated schema name"), - dataApiKey: optionalTextOption("data-api-key", "The Otto Data API key to use"), - proofkitToken: optionalTextOption( - "proofkit-token", - "ProofKit session token to pass through to local FileMaker MCP setup", - ), - dataSource: optionalChoiceOption("data-source", ["filemaker", "none"] as const, "The data source to use"), - noGit: booleanOption("no-git").pipe(withOptionDescription("Skip git initialization")), - noInstall: booleanOption("no-install").pipe(withOptionDescription("Skip package installation")), - force: booleanOption("force").pipe( - withAlias("f"), - withOptionDescription("Force overwrite target directory when it already contains files"), - ), - CI: booleanOption("ci").pipe(withOptionDescription("Deprecated alias for --non-interactive")), - nonInteractive: booleanOption("non-interactive").pipe( - withOptionDescription("Never prompt for input; fail when required values are missing"), - ), - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - ({ dir, ...options }) => { - const nonInteractive = resolveNonInteractiveMode({ - CI: options.CI, - nonInteractive: options.nonInteractive, - ...getCurrentTTYState(), - }); - - const flags: CliFlags = { - ...defaultCliFlags, - appType: getOrUndefined(options.appType), - server: getOrUndefined(options.server), - adminApiKey: getOrUndefined(options.adminApiKey), - fileName: getOrUndefined(options.fileName), - layoutName: getOrUndefined(options.layoutName), - schemaName: getOrUndefined(options.schemaName), - dataApiKey: getOrUndefined(options.dataApiKey), - proofkitToken: getOrUndefined(options.proofkitToken), - dataSource: getOrUndefined(options.dataSource), - noGit: options.noGit, - noInstall: options.noInstall, - force: options.force, - CI: options.CI, - nonInteractive: options.nonInteractive, - debug: options.debug, - }; - - return makeLiveLayer({ - cwd: process.cwd(), - debug: flags.debug === true, - nonInteractive, - })(runInit(getOrUndefined(dir), flags)); - }, - ).pipe(withCommandDescription("Create a new project with ProofKit")); -} - -function makeAddCommand() { - return makeCommand( - "add", - { - name: optionalArg(textArg({ name: "name" })).pipe(withArgDescription("Supported add target, currently `addon`")), - target: optionalArg(textArg({ name: "target" })).pipe(withArgDescription("Add-on target")), - noInstall: booleanOption("no-install").pipe(withOptionDescription("Skip package installation")), - CI: booleanOption("ci").pipe(withOptionDescription("Deprecated alias for --non-interactive")), - nonInteractive: booleanOption("non-interactive").pipe( - withOptionDescription("Never prompt for input; fail when required values are missing"), - ), - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - ({ name, target, noInstall, CI, nonInteractive, debug }) => - legacyEffect( - async () => { - const [{ runAdd }, { initProgramState, state }] = await Promise.all([ - import("~/cli/add/index.js"), - import("~/state.js"), - ]); - initProgramState({ - noInstall, - ci: CI, - nonInteractive, - debug, - }); - state.baseCommand = "add"; - state.projectDir = process.cwd(); - await runAdd(getOrUndefined(name), { - noInstall, - target: getOrUndefined(target), - }); - }, - { nonInteractive: CI || nonInteractive, debug }, - ), - ).pipe(withCommandDescription("Add a supported ProofKit add-on.")); -} - -function makeRemoveCommand() { - return makeCommand( - "remove", - { - name: optionalArg(textArg({ name: "name" })).pipe(withArgDescription("Component type to remove")), - CI: booleanOption("ci").pipe(withOptionDescription("Deprecated alias for --non-interactive")), - nonInteractive: booleanOption("non-interactive").pipe( - withOptionDescription("Never prompt for input; fail when required values are missing"), - ), - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - ({ name, CI, nonInteractive, debug }) => - legacyEffect( - async () => { - const [{ runRemove }, { initProgramState, state }] = await Promise.all([ - import("~/cli/remove/index.js"), - import("~/state.js"), - ]); - initProgramState({ - ci: CI, - nonInteractive, - debug, - }); - state.baseCommand = "remove"; - state.projectDir = process.cwd(); - await runRemove(getOrUndefined(name)); - }, - { nonInteractive: CI || nonInteractive, debug }, - ), - ).pipe(withCommandDescription("Legacy command. Prefer direct code edits or package-native tools.")); -} - -function makeTypegenCommand() { - return makeCommand( - "typegen", - { - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - ({ debug }) => - legacyEffect( - async () => { - const [{ runTypegen }, { state }] = await Promise.all([ - import("~/cli/typegen/index.js"), - import("~/state.js"), - ]); - state.projectDir = process.cwd(); - await runTypegen({ - settings: (await import("~/utils/parseSettings.js")).getSettings(), - }); - }, - { debug }, - ), - ).pipe(withCommandDescription("Legacy alias. Prefer `npx @proofkit/typegen`.")); -} - -function makeDeployCommand() { - return makeCommand( - "deploy", - { - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - ({ debug }) => - legacyEffect( - async () => { - const [{ runDeploy }, { initProgramState, state }] = await Promise.all([ - import("~/cli/deploy/index.js"), - import("~/state.js"), - ]); - initProgramState({ debug }); - state.baseCommand = "deploy"; - state.projectDir = process.cwd(); - await runDeploy(); - }, - { debug }, - ), - ).pipe(withCommandDescription("Deploy your app")); -} - -function makeUpgradeCommand() { - return makeCommand( - "upgrade", - { - CI: booleanOption("ci").pipe(withOptionDescription("Deprecated alias for --non-interactive")), - nonInteractive: booleanOption("non-interactive").pipe( - withOptionDescription("Never prompt for input; fail when required values are missing"), - ), - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - ({ CI, nonInteractive, debug }) => - legacyEffect( - async () => { - const [{ runUpgrade }, { initProgramState, state }] = await Promise.all([ - import("~/cli/update/index.js"), - import("~/state.js"), - ]); - initProgramState({ ci: CI, nonInteractive, debug }); - state.baseCommand = "upgrade"; - state.projectDir = process.cwd(); - await runUpgrade(); - }, - { nonInteractive: CI || nonInteractive, debug }, - ), - ).pipe(withCommandDescription("Legacy command.")); -} - -function makeDoctorCommand() { - return makeCommand( - "doctor", - { - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - ({ debug }) => - makeLiveLayer({ - cwd: process.cwd(), - debug: debug === true, - nonInteractive: true, - })(runDoctor), - ).pipe(withCommandDescription("Inspect project health and suggest exact next steps")); -} - -function makePromptCommand() { - return makeCommand( - "prompt", - { - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - ({ debug }) => - makeLiveLayer({ - cwd: process.cwd(), - debug: debug === true, - nonInteractive: true, - })(runPrompt), - ).pipe(withCommandDescription("Agent workflow entrypoint placeholder")); -} - -const rootCommand = makeCommand( - cliName, - { - CI: booleanOption("ci").pipe(withOptionDescription("Deprecated alias for --non-interactive")), - nonInteractive: booleanOption("non-interactive").pipe( - withOptionDescription("Never prompt for input; fail when required values are missing"), - ), - debug: booleanOption("debug").pipe(withOptionDescription("Run in debug mode")), - }, - (options) => - makeLiveLayer({ - cwd: process.cwd(), - debug: options.debug === true, - nonInteractive: resolveNonInteractiveMode({ - CI: options.CI, - nonInteractive: options.nonInteractive, - ...getCurrentTTYState(), - }), - })( - runDefaultCommand({ - ...defaultCliFlags, - CI: options.CI, - nonInteractive: options.nonInteractive, - debug: options.debug, - }), - ), -).pipe( - withCommandDescription("Interactive CLI to scaffold and manage ProofKit projects"), - withSubcommands([ - makeInitCommand(), - makeDoctorCommand(), - makePromptCommand(), - makeAddCommand(), - makeRemoveCommand(), - makeTypegenCommand(), - makeDeployCommand(), - makeUpgradeCommand(), - ]), -); - -export const cli = run(rootCommand, { - name: "ProofKit", - version: getCliVersion(), -}); - -function isMainEntrypoint(argvPath: string | undefined, moduleUrl: string) { - if (!argvPath) { - return false; - } - - const resolvedModulePath = fileURLToPath(moduleUrl); - - try { - return realpathSync(argvPath) === realpathSync(resolvedModulePath); - } catch { - return path.resolve(argvPath) === path.resolve(resolvedModulePath); - } -} - -const isMainModule = isMainEntrypoint(process.argv[1], import.meta.url); - -const debugFlagNames = new Set(["--debug"]); -const versionFlagNames = new Set(["-v", "--version"]); - -function shouldShowDebugDetails(argv: readonly string[]) { - return argv.some((arg) => debugFlagNames.has(arg)); -} - -function isVersionRequest(argv: readonly string[]) { - const args = argv.slice(2); - return args.length === 1 && versionFlagNames.has(args[0] ?? ""); -} - -export function renderFailure(cause: Cause.Cause, showDebugDetails: boolean) { - const failure = getOrUndefined(Cause.failureOption(cause)); - - if (failure && isValidationError(failure)) { - if (showDebugDetails) { - console.error(`\n[debug] ${Cause.pretty(cause)}`); - } - return; - } - - if (failure && isCliError(failure)) { - console.error(getCliErrorMessage(failure)); - } else { - const error = Cause.squash(cause); - console.error(error instanceof Error ? error.message : String(error)); - } - - if (showDebugDetails) { - console.error(`\n[debug] ${Cause.pretty(cause)}`); - } -} - -async function main(argv: readonly string[]) { - const showDebugDetails = shouldShowDebugDetails(argv); - const exit = await Effect.runPromiseExit(Effect.provide(cli(argv), nodeContextLayer)); - - if (Exit.isFailure(exit)) { - renderFailure(exit.cause, showDebugDetails); - process.exitCode = 1; - } -} - -if (isMainModule) { - if (isVersionRequest(process.argv)) { - console.log(getCliVersion()); - process.exit(0); - } - - renderTitle(getCliVersion()); - main(process.argv).catch((error) => { - console.error(error instanceof Error ? error.message : String(error)); - process.exitCode = 1; - }); -} diff --git a/packages/cli/src/installers/auth-shared.ts b/packages/cli/src/installers/auth-shared.ts deleted file mode 100644 index 20f1401d..00000000 --- a/packages/cli/src/installers/auth-shared.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { type SourceFile, SyntaxKind } from "ts-morph"; - -import { ensureReturnStatementIsWrappedInFragment } from "~/utils/ts-morph.js"; - -export function addToHeaderSlot(slotSourceFile: SourceFile, importFrom: string) { - slotSourceFile.addImportDeclaration({ - defaultImport: "UserMenu", - moduleSpecifier: importFrom, - }); - - // ensure Group from @mantine/core is imported - const mantineCoreImport = slotSourceFile.getImportDeclaration( - (dec) => dec.getModuleSpecifierValue() === "@mantine/core", - ); - if (mantineCoreImport) { - const groupImport = mantineCoreImport.getNamedImports().find((imp) => imp.getName() === "Group"); - - if (!groupImport) { - mantineCoreImport.addNamedImport({ name: "Group" }); - } - } else { - slotSourceFile.addImportDeclaration({ - namedImports: [{ name: "Group" }], - moduleSpecifier: "@mantine/core", - }); - } - - const returnStatement = ensureReturnStatementIsWrappedInFragment( - slotSourceFile - .getFunction((dec) => dec.isDefaultExport()) - ?.getBody() - ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement), - ); - - const existingElements = returnStatement - ?.getFirstDescendantByKind(SyntaxKind.JsxOpeningFragment) - ?.getParentIfKind(SyntaxKind.JsxFragment) - ?.getFirstDescendantByKind(SyntaxKind.SyntaxList) - ?.getText(); - - if (!existingElements) { - console.log(`Failed to inject into header slot at ${slotSourceFile.getFilePath()}`); - return; - } - - returnStatement?.replaceWithText(`return (<>${existingElements})`); - returnStatement?.formatText(); - slotSourceFile.saveSync(); -} diff --git a/packages/cli/src/installers/better-auth.ts b/packages/cli/src/installers/better-auth.ts deleted file mode 100644 index f417ab36..00000000 --- a/packages/cli/src/installers/better-auth.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function betterAuthInstaller() { - // TODO: Implement better-auth installer -} diff --git a/packages/cli/src/installers/clerk.ts b/packages/cli/src/installers/clerk.ts deleted file mode 100644 index 11ddd816..00000000 --- a/packages/cli/src/installers/clerk.ts +++ /dev/null @@ -1,153 +0,0 @@ -import path from "node:path"; -import chalk from "chalk"; -import fs from "fs-extra"; -import { type SourceFile, SyntaxKind } from "ts-morph"; - -import { PKG_ROOT } from "~/consts.js"; -import { addPackageDependency } from "~/utils/addPackageDependency.js"; -import { addToEnv } from "~/utils/addToEnvs.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; -import { addToHeaderSlot } from "./auth-shared.js"; - -export const clerkInstaller = async ({ projectDir }: { projectDir: string }) => { - addPackageDependency({ - projectDir, - dependencies: ["@clerk/nextjs", "@clerk/themes"], - devMode: false, - }); - - // add clerk middleware - // check if middleware already exists, if not add it - const extrasDir = path.join(PKG_ROOT, "template/extras"); - - const middlewareDest = path.join(projectDir, "src/middleware.ts"); - if (fs.existsSync(middlewareDest)) { - // throw new Error("Middleware already exists"); - console.log( - chalk.yellow( - "Middleware already exists. To require auth for your app, be sure to follow the guide to setup Clerk middleware. https://clerk.com/docs/references/nextjs/clerk-middleware#clerk-middleware-next-js", - ), - ); - } else { - const middlewareSrc = path.join(extrasDir, "src/middleware/clerk.ts"); - fs.copySync(middlewareSrc, middlewareDest); - } - - // copy auth pages - fs.copySync(path.join(extrasDir, "src/app/clerk-auth"), path.join(projectDir, "src/app/auth")); - - // copy auth components - fs.copySync(path.join(extrasDir, "src/components/clerk-auth"), path.join(projectDir, "src/components/clerk-auth")); - - // add ClerkProvider to app layout - const layoutFile = path.join(projectDir, "src/app/layout.tsx"); - const project = getNewProject(projectDir); - addClerkProvider(project.addSourceFileAtPath(layoutFile)); - - // inject signin/signout components to header slots - addToHeaderSlot( - project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-right.tsx")), - "@/components/clerk-auth/user-menu", - ); - addToHeaderSlot( - project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-mobile-content.tsx")), - "@/components/clerk-auth/user-menu-mobile", - ); - - addToSafeActionClient(project.addSourceFileAtPathIfExists(path.join(projectDir, "src/server/safe-action.ts"))); - - // add envs to .env and .env.schema - await addToEnv({ - projectDir, - project, - envs: [ - { - name: "NEXT_PUBLIC_CLERK_SIGN_IN_URL", - zodValue: "z.string()", - defaultValue: "/auth/signin", - type: "client", - }, - { - name: "NEXT_PUBLIC_CLERK_SIGN_UP_URL", - zodValue: "z.string()", - defaultValue: "/auth/signup", - type: "client", - }, - { - name: "CLERK_SECRET_KEY", - zodValue: `z.string().startsWith('sk_').min(1, { - message: - "No Clerk Secret Key found. Did you create your Clerk app and copy the environment variables to you .env file?", - })`, - type: "server", - }, - { - name: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", - zodValue: `z.string().startsWith('pk_').min(1, { - message: - "No Clerk Public Key found. Did you create your Clerk app and copy the environment variables to you .env file?", - })`, - type: "client", - }, - ], - envFileDescription: - "Hosted auth with Clerk. Set up a new app at https://dashboard.clerk.com/apps/new to get these values.", - }); - - await formatAndSaveSourceFiles(project); -}; - -export function addClerkProvider(sourceFile: SourceFile) { - sourceFile.addImportDeclaration({ - namedImports: [{ name: "ClerkAuthProvider" }], - moduleSpecifier: "@/components/clerk-auth/clerk-provider", - }); - - // Step 2: Wrap default exported function's return statement with ClerkProvider - const exportDefault = sourceFile.getFunction((dec) => dec.isDefaultExport()); - - // find the mantine provider in this export - const mantineProvider = exportDefault - ?.getBody() - ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement) - ?.getDescendantsOfKind(SyntaxKind.JsxOpeningElement) - .find((openingElement) => openingElement.getTagNameNode().getText() === "MantineProvider") - ?.getParentIfKind(SyntaxKind.JsxElement); - - const childrenText = mantineProvider - ?.getJsxChildren() - .map((child) => child.getText()) - .filter(Boolean) - .join("\n"); - - mantineProvider?.getChildSyntaxList()?.replaceWithText( - ` - ${childrenText} - `, - ); -} - -function addToSafeActionClient(sourceFile?: SourceFile) { - if (!sourceFile) { - console.log(chalk.yellow("Failed to inject into safe-action-client. Did you move the safe-action.ts file?")); - return; - } - - sourceFile.addImportDeclaration({ - namedImports: [{ name: "auth", alias: "getAuth" }], - moduleSpecifier: "@clerk/nextjs/server", - }); - - // add to end of file - sourceFile.addStatements((writer) => - writer.writeLine(`export const authedActionClient = actionClient.use(async ({ next, ctx }) => { - const auth = getAuth(); - if (!auth.userId) { - throw new Error("Unauthorized"); - } - return next({ ctx: { ...ctx, auth } }); -}); - -`), - ); -} diff --git a/packages/cli/src/installers/dependencyVersionMap.ts b/packages/cli/src/installers/dependencyVersionMap.ts deleted file mode 100644 index 37badaf9..00000000 --- a/packages/cli/src/installers/dependencyVersionMap.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { - getFmdapiVersion, - getNodeMajorVersion, - getProofkitBetterAuthVersion, - getProofkitDependencyVersion, - getProofkitWebviewerVersion, - getTypegenVersion, - getVersion, -} from "~/utils/getProofKitVersion.js"; - -/* - * This maps the necessary packages to a version. - * This improves performance significantly over fetching it from the npm registry. - */ -export const dependencyVersionMap = { - "@proofkit/fmdapi": getProofkitDependencyVersion(getFmdapiVersion()), - "@proofkit/webviewer": getProofkitDependencyVersion(getProofkitWebviewerVersion()), - "@proofkit/cli": getProofkitDependencyVersion(getVersion()), - "@proofkit/typegen": getProofkitDependencyVersion(getTypegenVersion()), - "@proofkit/better-auth": getProofkitDependencyVersion(getProofkitBetterAuthVersion()), - - // NextAuth.js - "next-auth": "beta", - "next-auth-adapter-filemaker": "beta", - - "@auth/prisma-adapter": "^1.6.0", - "@auth/drizzle-adapter": "^1.1.0", - - // Prisma - prisma: "^5.14.0", - "@prisma/client": "^5.14.0", - "@prisma/adapter-planetscale": "^5.14.0", - - // Drizzle - "drizzle-orm": "^0.30.10", - "drizzle-kit": "^0.21.4", - mysql2: "^3.9.7", - "@planetscale/database": "^1.18.0", - postgres: "^3.4.4", - "@libsql/client": "^0.6.0", - - // TailwindCSS - tailwindcss: "^4.1.10", - postcss: "^8.4.41", - "@tailwindcss/postcss": "^4.1.10", - "@tailwindcss/vite": "^4.2.1", - "class-variance-authority": "^0.7.1", - clsx: "^2.1.1", - "tailwind-merge": "^3.5.0", - "tw-animate-css": "^1.4.0", - - // tRPC - "@trpc/client": "^11.0.0-rc.446", - "@trpc/server": "^11.0.0-rc.446", - "@trpc/react-query": "^11.0.0-rc.446", - "@trpc/next": "^11.0.0-rc.446", - superjson: "^2.2.1", - "server-only": "^0.0.1", - - // Clerk - "@clerk/nextjs": "^6.3.1", - "@clerk/themes": "^2.1.33", - - // Tanstack Query - "@tanstack/react-query": "^5.59.0", - "@tanstack/react-query-devtools": "^5.59.0", - - // ProofKit Auth - "@node-rs/argon2": "^2.0.2", - "@oslojs/binary": "^1.0.0", - "@oslojs/crypto": "^1.0.1", - "@oslojs/encoding": "^1.1.0", - "js-cookie": "^3.0.5", - "@types/js-cookie": "^3.0.6", - - // React Email - "@react-email/components": "^0.5.0", - "@react-email/render": "1.2.0", - "@react-email/preview-server": "^4.2.8", - "@plunk/node": "^3.0.3", - "react-email": "^4.2.8", - resend: "^4.0.0", - "@sendgrid/mail": "^8.1.4", - - // Node - "@types/node": `^${getNodeMajorVersion()}`, - - // Radix (for shadcn/ui) - "@radix-ui/react-slot": "^1.2.3", - - // Icons (for shadcn/ui) - "lucide-react": "^1.16.0", - - // better-auth - "better-auth": "^1.3.4", - "@daveyplate/better-auth-ui": "^2.1.3", - - // Mantine UI - "@mantine/core": "^7.15.0", - "@mantine/dates": "^7.15.0", - "@mantine/hooks": "^7.15.0", - "@mantine/modals": "^7.15.0", - "@mantine/notifications": "^7.15.0", - "mantine-react-table": "^2.0.0", - - // Theme utilities - "next-themes": "^0.4.6", - - // Linting and formatting - oxlint: "^1.39.0", - ultracite: "^7.0.0", - - // Zod - zod: "^4", -} as const; -export type AvailableDependencies = keyof typeof dependencyVersionMap; diff --git a/packages/cli/src/installers/envVars.ts b/packages/cli/src/installers/envVars.ts deleted file mode 100644 index ebcd8cd9..00000000 --- a/packages/cli/src/installers/envVars.ts +++ /dev/null @@ -1,43 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; - -import type { Installer } from "~/installers/index.js"; -import { state } from "~/state.js"; -import { logger } from "~/utils/logger.js"; - -export type FMAuthKeys = { username: string; password: string } | { ottoApiKey: string }; - -export const initEnvFile: Installer = () => { - const envFilePath = findT3EnvFile(false) ?? "./src/config/env.ts"; - - const envContent = ` -# When adding additional environment variables, the schema in "${envFilePath}" -# should be updated accordingly. - -` - .trim() - .concat("\n"); - - const envDest = path.join(state.projectDir, ".env"); - - fs.writeFileSync(envDest, envContent, "utf-8"); -}; -export function findT3EnvFile(throwIfNotFound: false): string | null; -export function findT3EnvFile(throwIfNotFound?: true): string; -export function findT3EnvFile(throwIfNotFound?: boolean): string | null { - const possiblePaths = ["src/config/env.ts", "src/lib/env.ts", "src/env.ts", "lib/env.ts", "env.ts", "config/env.ts"]; - - for (const testPath of possiblePaths) { - const fullPath = path.join(state.projectDir, testPath); - if (fs.existsSync(fullPath)) { - return fullPath; - } - } - - if (throwIfNotFound === false) { - return null; - } - - logger.warn("Could not find T3 env files. Initialize them manually before continuing."); - throw new Error("T3 env file not found"); -} diff --git a/packages/cli/src/installers/index.ts b/packages/cli/src/installers/index.ts deleted file mode 100644 index b4fb6fb6..00000000 --- a/packages/cli/src/installers/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { initEnvFile } from "~/installers/envVars.js"; -import type { PackageManager } from "~/utils/getUserPkgManager.js"; - -// Turning this into a const allows the list to be iterated over for programmatically creating prompt options -// Should increase extensibility in the future -export const availablePackages = ["nextAuth", "trpc", "envVariables", "fmdapi", "webViewerFetch", "clerk"] as const; -export type AvailablePackages = (typeof availablePackages)[number]; - -export interface InstallerOptions { - pkgManager: PackageManager; - noInstall: boolean; - packages?: PkgInstallerMap; - projectName: string; - scopedAppName: string; -} - -export type Installer = (opts: InstallerOptions) => void; - -export type PkgInstallerMap = { - [pkg in AvailablePackages]?: { - inUse: boolean; - installer: Installer; - }; -}; - -export const buildPkgInstallerMap = (): PkgInstallerMap => ({ - envVariables: { - inUse: true, - installer: initEnvFile, - }, -}); diff --git a/packages/cli/src/installers/install-fm-addon.ts b/packages/cli/src/installers/install-fm-addon.ts deleted file mode 100644 index 9de407c0..00000000 --- a/packages/cli/src/installers/install-fm-addon.ts +++ /dev/null @@ -1,413 +0,0 @@ -import crypto from "node:crypto"; -import os from "node:os"; -import path from "node:path"; -import chalk from "chalk"; -import fs from "fs-extra"; - -import { openExternal } from "~/utils/browserOpen.js"; -import { requestArrayBuffer, requestJson } from "~/utils/http.js"; - -export type FmAddonName = "auth" | "wv"; -export type FmAddonInspectionStatus = "missing" | "installed-current" | "installed-outdated" | "unknown"; - -export interface FmAddonInspection { - status: FmAddonInspectionStatus; - addonName: FmAddonName; - addonDir: string; - addonDisplayName: string; - installCommand: string; - targetDir: string | null; - installedPath: string | null; - remoteAssetUrl: string; - latestVersion?: string; - installedVersion?: string; - reason?: string; -} - -type FmAddonTarget = "webviewer" | "auth"; - -interface FmAddonManifestAsset { - file?: string; - url?: string; - sha256?: string; - size?: number; -} - -interface FmAddonManifestEntry { - version?: string; - latestVersion?: string; - assets?: FmAddonManifestAsset[]; - url?: string; - file?: string; -} - -interface FmAddonManifest { - product?: string; - updatedAt?: string; - latestVersion?: string; - addons?: Partial>; - versions?: Array<{ - version?: string; - assets?: FmAddonManifestAsset[]; - addons?: Partial>; - }>; -} - -const DEFAULT_FM_ADDON_MANIFEST_URL = "https://downloads.ottomatic.cloud/proofkit/manifest.json"; -const FM_ADDON_VERSION_REGEX = /]*\bversion="([^"]+)"/i; -const NUMERIC_VERSION_PART_REGEX = /^\d+$/; - -function getAddonDisplayName(addonName: FmAddonName) { - return addonName === "auth" ? "FM Auth Add-on" : "ProofKit Web Viewer"; -} - -function getAddonDir(addonName: FmAddonName) { - return addonName === "auth" ? "ProofKitAuth" : "ProofKitWV"; -} - -function getAddonTarget(addonName: FmAddonName): FmAddonTarget { - return addonName === "auth" ? "auth" : "webviewer"; -} - -function getAddonInstallCommand(addonName: FmAddonName) { - return addonName === "auth" ? "proofkit add addon auth" : "proofkit add addon webviewer"; -} - -function getAddonManifestUrl() { - return process.env.PROOFKIT_FM_ADDON_MANIFEST_URL || DEFAULT_FM_ADDON_MANIFEST_URL; -} - -export function resolveFmAddonDownloadDir(homeDir = os.homedir()): string { - return process.env.PROOFKIT_FM_ADDON_DOWNLOAD_DIR || path.join(homeDir, "Downloads", "ProofKit"); -} - -function parseAddonVersion(version: string) { - const parts = version - .split(".") - .map((part) => part.trim()) - .filter(Boolean); - - if (parts.length === 0 || parts.some((part) => !NUMERIC_VERSION_PART_REGEX.test(part))) { - return undefined; - } - - return parts.map((part) => Number.parseInt(part, 10)); -} - -export function compareAddonVersions(installedVersion: string, latestVersion: string) { - const installed = parseAddonVersion(installedVersion); - const latest = parseAddonVersion(latestVersion); - - if (!(installed && latest)) { - return undefined; - } - - const maxLength = Math.max(installed.length, latest.length); - for (let index = 0; index < maxLength; index += 1) { - const installedPart = installed[index] ?? 0; - const latestPart = latest[index] ?? 0; - - if (installedPart < latestPart) { - return -1; - } - if (installedPart > latestPart) { - return 1; - } - } - - return 0; -} - -async function readAddonVersionFromDirectory(addonPath: string): Promise { - const sidecarJsonPath = `${addonPath}.proofkit.json`; - if (await fs.pathExists(sidecarJsonPath)) { - const sidecarJson = (await fs.readJson(sidecarJsonPath)) as { - version?: string | number; - }; - if (typeof sidecarJson.version === "string" || typeof sidecarJson.version === "number") { - return String(sidecarJson.version); - } - } - - const templateXmlPath = path.join(addonPath, "template.xml"); - if (await fs.pathExists(templateXmlPath)) { - const templateXml = await fs.readFile(templateXmlPath, "utf8"); - const versionMatch = templateXml.match(FM_ADDON_VERSION_REGEX); - if (versionMatch?.[1]) { - return versionMatch[1]; - } - } - - const infoJsonPath = path.join(addonPath, "info.json"); - if (await fs.pathExists(infoJsonPath)) { - const infoJson = (await fs.readJson(infoJsonPath)) as { - Version?: string | number; - }; - if (typeof infoJson.Version === "string" || typeof infoJson.Version === "number") { - return String(infoJson.Version); - } - } - - return undefined; -} - -function resolveUrl(url: string, baseUrl: string) { - return new URL(url, baseUrl).toString(); -} - -function getRemoteFileName(remoteAssetUrl: string) { - try { - return path.basename(new URL(remoteAssetUrl).pathname); - } catch { - return path.basename(remoteAssetUrl); - } -} - -function pickAddonEntry(manifest: FmAddonManifest, addonName: FmAddonName): FmAddonManifestEntry | undefined { - const target = getAddonTarget(addonName); - const latestVersion = manifest.latestVersion; - - if (latestVersion && manifest.versions?.length) { - const latestEntry = manifest.versions.find((entry) => entry.version === latestVersion); - const addon = latestEntry?.addons?.[target]; - if (addon) { - return { ...addon, version: addon.version ?? latestEntry?.version }; - } - if (latestEntry?.assets?.length) { - return { version: latestEntry.version, assets: latestEntry.assets }; - } - } - - return manifest.addons?.[target]; -} - -function pickAddonAsset(entry: FmAddonManifestEntry): FmAddonManifestAsset | undefined { - const assets = entry.assets ?? (entry.url ? [{ url: entry.url, file: entry.file }] : []); - return ( - assets.find((asset) => asset.file?.toLowerCase().endsWith(".fmaddon")) ?? - assets.find((asset) => asset.url?.toLowerCase().endsWith(".fmaddon")) ?? - assets[0] - ); -} - -export async function resolveRemoteFmAddon(addonName: FmAddonName) { - const manifestUrl = getAddonManifestUrl(); - const response = await requestJson(manifestUrl); - if (response.status < 200 || response.status >= 300) { - throw new Error(`Could not fetch FileMaker add-on manifest (${response.status}).`); - } - - const entry = pickAddonEntry(response.data, addonName); - const asset = entry ? pickAddonAsset(entry) : undefined; - if (!(entry && asset?.url)) { - throw new Error(`Manifest does not include a ${getAddonDisplayName(addonName)} asset.`); - } - - return { - version: entry.version ?? entry.latestVersion ?? response.data.latestVersion, - url: resolveUrl(asset.url, manifestUrl), - file: asset.file, - sha256: asset.sha256, - }; -} - -export async function inspectFmAddon( - { - addonName, - }: { - addonName: FmAddonName; - }, - options?: { - targetDir?: string | null; - latestAddonPath?: string; - }, -): Promise { - const addonDir = getAddonDir(addonName); - const addonDisplayName = getAddonDisplayName(addonName); - const installCommand = getAddonInstallCommand(addonName); - const targetDir = options && "targetDir" in options ? options.targetDir : resolveFmAddonDownloadDir(); - const remoteAddon = options?.latestAddonPath - ? { - latestVersion: await readAddonVersionFromDirectory(options.latestAddonPath), - remoteAssetUrl: options.latestAddonPath, - } - : await resolveRemoteFmAddon(addonName) - .then((addon) => ({ - latestVersion: addon.version, - remoteAssetUrl: addon.url, - })) - .catch((error) => ({ - latestVersion: undefined, - remoteAssetUrl: getAddonManifestUrl(), - reason: error instanceof Error ? error.message : "remote-manifest-unavailable", - })); - - if (!targetDir) { - return { - status: "unknown", - addonName, - addonDir, - addonDisplayName, - installCommand, - targetDir: null, - installedPath: null, - remoteAssetUrl: remoteAddon.remoteAssetUrl, - latestVersion: remoteAddon.latestVersion, - reason: "unsupported-platform", - }; - } - - const remoteFileName = getRemoteFileName(remoteAddon.remoteAssetUrl); - const installedCandidates = [ - remoteFileName ? path.join(targetDir, remoteFileName) : undefined, - path.join(targetDir, "ProofKit.fmaddon"), - path.join(targetDir, `${addonDir}.fmaddon`), - path.join(targetDir, addonDir), - ].filter((candidate): candidate is string => Boolean(candidate)); - const installedPath = ( - await Promise.all( - installedCandidates.map(async (candidate) => ((await fs.pathExists(candidate)) ? candidate : null)), - ) - ).find((candidate): candidate is string => Boolean(candidate)); - if (!installedPath) { - return { - status: "missing", - addonName, - addonDir, - addonDisplayName, - installCommand, - targetDir, - installedPath: installedCandidates[0] ?? null, - remoteAssetUrl: remoteAddon.remoteAssetUrl, - latestVersion: remoteAddon.latestVersion, - }; - } - - const installedVersion = await readAddonVersionFromDirectory(installedPath); - if (!(installedVersion && remoteAddon.latestVersion)) { - return { - status: "unknown", - addonName, - addonDir, - addonDisplayName, - installCommand, - targetDir, - installedPath, - remoteAssetUrl: remoteAddon.remoteAssetUrl, - latestVersion: remoteAddon.latestVersion, - installedVersion, - reason: installedVersion ? "remote-version-unavailable" : "unreadable-version", - }; - } - - const comparison = compareAddonVersions(installedVersion, remoteAddon.latestVersion); - if (comparison === undefined) { - return { - status: "unknown", - addonName, - addonDir, - addonDisplayName, - installCommand, - targetDir, - installedPath, - remoteAssetUrl: remoteAddon.remoteAssetUrl, - latestVersion: remoteAddon.latestVersion, - installedVersion, - reason: "invalid-version", - }; - } - - return { - status: comparison < 0 ? "installed-outdated" : "installed-current", - addonName, - addonDir, - addonDisplayName, - installCommand, - targetDir, - installedPath, - remoteAssetUrl: remoteAddon.remoteAssetUrl, - latestVersion: remoteAddon.latestVersion, - installedVersion, - }; -} - -export function getFmAddonInstallInstructions(addonName: FmAddonName) { - const addonDisplayName = getAddonDisplayName(addonName); - const installCommand = getAddonInstallCommand(addonName); - return { - addonDisplayName, - installCommand, - docsUrl: - addonName === "auth" ? "https://proofkit.proof.sh/auth/fm-addon" : "https://proofkit.proof.sh/docs/webviewer", - steps: [ - `Run \`${installCommand}\` to download and open the latest add-on`, - "When FileMaker opens the add-on file, confirm the install prompt", - `Open your FileMaker file, go to layout mode, and add the ${addonDisplayName} add-on to the file`, - ], - }; -} - -export async function installFmAddonExplicitly({ addonName }: { addonName: FmAddonName }) { - const addonDisplayName = getAddonDisplayName(addonName); - const addonDir = getAddonDir(addonName); - - const remoteAddon = await resolveRemoteFmAddon(addonName); - const addonResponse = await requestArrayBuffer(remoteAddon.url); - if (addonResponse.status < 200 || addonResponse.status >= 300) { - throw new Error(`Could not download ${addonDisplayName} (${addonResponse.status}).`); - } - if (remoteAddon.sha256) { - const digest = crypto.createHash("sha256").update(addonResponse.data).digest("hex"); - if (digest !== remoteAddon.sha256) { - throw new Error(`Downloaded ${addonDisplayName} checksum did not match the manifest.`); - } - } - - const targetDir = resolveFmAddonDownloadDir(); - await fs.ensureDir(targetDir); - const addonPath = path.join(targetDir, remoteAddon.file || `${addonDir}.fmaddon`); - await fs.writeFile(addonPath, addonResponse.data); - await fs.writeJson( - `${addonPath}.proofkit.json`, - { - version: remoteAddon.version, - url: remoteAddon.url, - sha256: remoteAddon.sha256, - installedAt: new Date().toISOString(), - }, - { spaces: 2 }, - ); - - if (process.env.PROOFKIT_SKIP_OPEN_FM_ADDON !== "1") { - await openExternal(addonPath); - } - - console.log(""); - console.log(chalk.bgYellow(" ACTION REQUIRED: ")); - if (addonName === "auth") { - console.log( - `${chalk.yellowBright( - "The FM Auth add-on file was downloaded and opened.", - )} ${chalk.dim("(Learn more: https://proofkit.proof.sh/auth/fm-addon)")}`, - ); - } else { - console.log( - `${chalk.yellowBright( - "The ProofKit Web Viewer add-on file was downloaded and opened.", - )} ${chalk.dim("(Learn more: https://proofkit.proof.sh/docs/webviewer)")}`, - ); - } - const steps = [ - "When FileMaker opens the add-on file, confirm the install prompt", - `Open your FileMaker file, go to layout mode, and add the ${addonDisplayName} add-on to the file`, - `If FileMaker did not open automatically, open ${addonPath}`, - ]; - steps.forEach((step, index) => { - console.log(`${index + 1}. ${step}`); - }); - return true; -} - -export function installFmAddon({ addonName }: { addonName: FmAddonName }) { - return installFmAddonExplicitly({ addonName }); -} diff --git a/packages/cli/src/installers/nextAuth.ts b/packages/cli/src/installers/nextAuth.ts deleted file mode 100644 index 163df0f9..00000000 --- a/packages/cli/src/installers/nextAuth.ts +++ /dev/null @@ -1,189 +0,0 @@ -import path from "node:path"; -import chalk from "chalk"; -import fs from "fs-extra"; -import ora from "ora"; -import { type SourceFile, SyntaxKind } from "ts-morph"; - -import { PKG_ROOT } from "~/consts.js"; -import { getExistingSchemas } from "~/generators/fmdapi.js"; -import { _runExecCommand, generateRandomSecret } from "~/helpers/installDependencies.js"; -import { addPackageDependency } from "~/utils/addPackageDependency.js"; -import { addToEnv } from "~/utils/addToEnvs.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; -import { addToHeaderSlot } from "./auth-shared.js"; -import { dependencyVersionMap } from "./dependencyVersionMap.js"; - -export const nextAuthInstaller = async ({ projectDir }: { projectDir: string }) => { - addPackageDependency({ - projectDir, - dependencies: ["next-auth", "next-auth-adapter-filemaker"], - devMode: false, - }); - - const extrasDir = path.join(PKG_ROOT, "template/extras"); - - const routeHandlerFile = "src/app/api/auth/[...nextauth]/route.ts"; - const srcToUse = routeHandlerFile; - - const apiHandlerSrc = path.join(extrasDir, srcToUse); - const apiHandlerDest = path.join(projectDir, srcToUse); - fs.copySync(apiHandlerSrc, apiHandlerDest); - - const authConfigSrc = path.join(extrasDir, "src/server", "next-auth", "base.ts"); - const authConfigDest = path.join(projectDir, "src/server/auth.ts"); - fs.copySync(authConfigSrc, authConfigDest); - - const passwordSrc = path.join(extrasDir, "src/server", "next-auth", "password.ts"); - const passwordDest = path.join(projectDir, "src/server/password.ts"); - fs.copySync(passwordSrc, passwordDest); - - // copy users.ts to data directory - fs.copySync(path.join(extrasDir, "src/server/data/users.ts"), path.join(projectDir, "src/server/data/users.ts")); - - // copy auth pages - fs.copySync(path.join(extrasDir, "src/app/next-auth"), path.join(projectDir, "src/app/auth")); - - // copy auth components - fs.copySync(path.join(extrasDir, "src/components/next-auth"), path.join(projectDir, "src/components/next-auth")); - - const project = getNewProject(projectDir); - - // modify root layout to wrap with session provider - addNextAuthProviderToRootLayout(project.addSourceFileAtPath(path.join(projectDir, "src/app/layout.tsx"))); - - // inject signin/signout components to header slots - addToHeaderSlot( - project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-right.tsx")), - "@/components/next-auth/user-menu", - ); - addToHeaderSlot( - project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-mobile-content.tsx")), - "@/components/next-auth/user-menu-mobile", - ); - - // add a protected safe-action-client - addToSafeActionClient(project.addSourceFileAtPathIfExists(path.join(projectDir, "src/server/safe-action.ts"))); - - // // TODO do this part in-house, maybe with execa directly - // await runExecCommand({ - // command: ["auth", "secret"], - // projectDir, - // }); - - // add middleware - fs.copySync(path.join(extrasDir, "src/middleware/next-auth.ts"), path.join(projectDir, "src/middleware.ts")); - - // add envs to .env and .env.schema - await addToEnv({ - projectDir, - project, - envs: [ - { - name: "AUTH_SECRET", - zodValue: "z.string().min(1)", - defaultValue: generateRandomSecret(), - type: "server", - }, - ], - }); - - await checkForNextAuthLayouts(projectDir); - - await formatAndSaveSourceFiles(project); -}; - -function addNextAuthProviderToRootLayout(rootLayoutSource: SourceFile) { - // Add imports - rootLayoutSource.addImportDeclaration({ - namedImports: [{ name: "NextAuthProvider" }], - moduleSpecifier: "@/components/next-auth/next-auth-provider", - }); - rootLayoutSource.addImportDeclaration({ - namedImports: [{ name: "auth" }], - moduleSpecifier: "@/server/auth", - }); - - const exportDefault = rootLayoutSource.getFunction((dec) => dec.isDefaultExport()); - - // make the function async - exportDefault?.setIsAsync(true); - - // get the session server-side - exportDefault?.getFirstDescendantByKind(SyntaxKind.Block)?.insertStatements(0, "const session = await auth();"); - - // get the body element from the return statement - const bodyElement = exportDefault - ?.getBody() - ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement) - ?.getDescendantsOfKind(SyntaxKind.JsxOpeningElement) - .find((openingElement) => openingElement.getTagNameNode().getText() === "body") - ?.getParentIfKind(SyntaxKind.JsxElement); - - // wrap the body element with the next auth provider - bodyElement?.replaceWithText( - ` - ${bodyElement.getText()} - `, - ); - - rootLayoutSource.formatText(); - rootLayoutSource.saveSync(); -} - -function addToSafeActionClient(sourceFile?: SourceFile) { - if (!sourceFile) { - console.log(chalk.yellow("Failed to inject into safe-action-client. Did you move the safe-action.ts file?")); - return; - } - - sourceFile.addImportDeclaration({ - namedImports: [{ name: "auth" }], - moduleSpecifier: "@/server/auth", - }); - - // add to end of file - sourceFile.addStatements((writer) => - writer.writeLine(`export const authedActionClient = actionClient.use( - async ({ next, ctx }) => { - const session = await auth(); - if (!session) { - throw new Error("Unauthorized"); - } - return next({ ctx: { ...ctx, session } }); - } -); -`), - ); -} - -async function checkForNextAuthLayouts(projectDir: string) { - const existingLayouts = getExistingSchemas({ - projectDir, - dataSourceName: "filemaker", - }); - const nextAuthLayouts = ["nextauth_user", "nextauth_account", "nextauth_session", "nextauth_verificationToken"]; - - const allNextAuthLayoutsExist = nextAuthLayouts.every((layout) => - existingLayouts.some((l) => l.schemaName === layout), - ); - - if (allNextAuthLayoutsExist) { - return; - } - - const spinner = await _runExecCommand({ - command: [`next-auth-adapter-filemaker@${dependencyVersionMap["next-auth-adapter-filemaker"]}`, "install-addon"], - projectDir, - }); - - // If the spinner was used to show the progress, use succeed method on it - // If not, use the succeed on a new spinner - (spinner ?? ora()).succeed(chalk.green("Successfully installed next-auth addon for FileMaker")); - - console.log(""); - console.log(chalk.bgYellow(" ACTION REQUIRED: ")); - console.log( - `${chalk.yellowBright("You must now install the NextAuth addon in your FileMaker file.")} -Learn more: https://proofkit.proof.sh/auth/next-auth\n`, - ); -} diff --git a/packages/cli/src/installers/proofkit-auth.ts b/packages/cli/src/installers/proofkit-auth.ts deleted file mode 100644 index 0179e09d..00000000 --- a/packages/cli/src/installers/proofkit-auth.ts +++ /dev/null @@ -1,219 +0,0 @@ -import path from "node:path"; -import type { OttoAPIKey } from "@proofkit/fmdapi"; -import chalk from "chalk"; -import dotenv from "dotenv"; -import fs from "fs-extra"; -import ora, { type Ora } from "ora"; -import { type SourceFile, SyntaxKind } from "ts-morph"; -import { getLayouts } from "~/cli/fmdapi.js"; -import * as p from "~/cli/prompts.js"; -import { abortIfCancel } from "~/cli/utils.js"; -import { PKG_ROOT } from "~/consts.js"; -import { UserCancelledError } from "~/core/errors.js"; -import { addConfig, runCodegenCommand } from "~/generators/fmdapi.js"; -import { injectTanstackQuery } from "~/generators/tanstack-query.js"; -import { state } from "~/state.js"; -import { addPackageDependency } from "~/utils/addPackageDependency.js"; -import { getSettings } from "~/utils/parseSettings.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; -import { addToHeaderSlot } from "./auth-shared.js"; -import { installFmAddon } from "./install-fm-addon.js"; -import { installReactEmail } from "./react-email.js"; - -export const proofkitAuthInstaller = async () => { - const spinner = ora("Installing files for auth...").start(); - - const projectDir = state.projectDir; - addPackageDependency({ - projectDir, - dependencies: ["@node-rs/argon2", "@oslojs/binary", "@oslojs/crypto", "@oslojs/encoding", "js-cookie"], - devMode: false, - }); - - addPackageDependency({ - projectDir, - dependencies: ["@types/js-cookie"], - devMode: true, - }); - - // copy all files from template/extras/fmaddon-auth to projectDir/src - await fs.copy(path.join(PKG_ROOT, "template/extras/fmaddon-auth"), path.join(projectDir, "src")); - - const project = getNewProject(projectDir); - - // ensure tanstack query is installed - await injectTanstackQuery({ project }); - - // inject signin/signout components to header slots - addToHeaderSlot( - project.addSourceFileAtPath(path.join(projectDir, "src/components/AppShell/slot-header-right.tsx")), - "@/components/auth/user-menu", - ); - // addToHeaderSlot( - // project.addSourceFileAtPath( - // path.join( - // projectDir, - // "src/components/AppShell/slot-header-mobile-content.tsx" - // ) - // ), - // "@/components/clerk-auth/user-menu-mobile" - // ); - - addToSafeActionClient(project.addSourceFileAtPathIfExists(path.join(projectDir, "src/server/safe-action.ts"))); - - await addConfig({ - config: { - type: "fmdapi", - envNames: undefined, - clientSuffix: "Layout", - layouts: [ - { - layoutName: "proofkit_auth_sessions", - schemaName: "sessions", - strictNumbers: true, - }, - { - layoutName: "proofkit_auth_users", - schemaName: "users", - strictNumbers: true, - }, - { - layoutName: "proofkit_auth_email_verification", - schemaName: "emailVerification", - strictNumbers: true, - }, - { - layoutName: "proofkit_auth_password_reset", - schemaName: "passwordReset", - strictNumbers: true, - }, - ], - clearOldFiles: true, - validator: false, - path: "./src/server/auth/db", - }, - projectDir, - runCodegen: false, - }); - - // install email files based on the email provider in state - await installReactEmail({ project, installServerFiles: true }); - - protectMainLayout(project.addSourceFileAtPath(path.join(projectDir, "src/app/(main)/layout.tsx"))); - - await formatAndSaveSourceFiles(project); - - let hasProofKitLayouts = false; - while (!hasProofKitLayouts) { - hasProofKitLayouts = await checkForProofKitLayouts(projectDir, spinner); - - if (hasProofKitLayouts) { - spinner.text = "Successfully detected all required layouts in your FileMaker file."; - } else { - const shouldContinue = abortIfCancel( - await p.confirm({ - message: "I have followed the above instructions, continue installing", - initialValue: true, - }), - ); - - if (!shouldContinue) { - throw new UserCancelledError({ message: "User aborted the operation" }); - } - } - } - await runCodegenCommand(); - - spinner.succeed("Auth installed successfully"); -}; - -function addToSafeActionClient(sourceFile?: SourceFile) { - if (!sourceFile) { - console.log(chalk.yellow("Failed to inject into safe-action-client. Did you move the safe-action.ts file?")); - return; - } - - sourceFile.addImportDeclaration({ - namedImports: [{ name: "getCurrentSession" }], - moduleSpecifier: "./auth/utils/session", - }); - - // add to end of file - sourceFile.addStatements((writer) => - writer.writeLine(`export const authedActionClient = actionClient.use(async ({ next, ctx }) => { - const { session, user } = await getCurrentSession(); - if (session === null) { - throw new Error("Unauthorized"); - } - - return next({ ctx: { ...ctx, session, user } }); -}); -`), - ); -} - -function protectMainLayout(sourceFile: SourceFile) { - sourceFile.addImportDeclaration({ - defaultImport: "Protect", - moduleSpecifier: "@/components/auth/protect", - }); - - // inject query provider into the root layout - - const exportDefault = sourceFile.getFunction((dec) => dec.isDefaultExport()); - const bodyElement = exportDefault - ?.getBody() - ?.getFirstDescendantByKind(SyntaxKind.ReturnStatement) - ?.getFirstDescendantByKind(SyntaxKind.JsxElement); - - bodyElement?.replaceWithText( - ` - ${bodyElement?.getText()} - `, - ); -} - -async function checkForProofKitLayouts(projectDir: string, spinner: Ora): Promise { - const settings = getSettings(); - - const dataSource = settings.dataSources.filter((s) => s.type === "fm").find((s) => s.name === "filemaker"); - - if (!dataSource) { - return false; - } - if (settings.envFile) { - dotenv.config({ - path: path.join(projectDir, settings.envFile), - }); - } - const dataApiKey = process.env[dataSource.envNames.apiKey]; - const fmFile = process.env[dataSource.envNames.database]; - const server = process.env[dataSource.envNames.server]; - - if (!(dataApiKey && fmFile && server)) { - return false; - } - - const existingLayouts = await getLayouts({ - dataApiKey: dataApiKey as OttoAPIKey, - fmFile, - server, - }); - const proofkitAuthLayouts = [ - "proofkit_auth_sessions", - "proofkit_auth_users", - "proofkit_auth_email_verification", - "proofkit_auth_password_reset", - ]; - - const allProofkitAuthLayoutsExist = proofkitAuthLayouts.every((layout) => existingLayouts.some((l) => l === layout)); - - if (allProofkitAuthLayoutsExist) { - return true; - } - - spinner.warn("Required layouts not found"); - await installFmAddon({ addonName: "auth" }); - - return false; -} diff --git a/packages/cli/src/installers/proofkit-webviewer.ts b/packages/cli/src/installers/proofkit-webviewer.ts deleted file mode 100644 index 52743e31..00000000 --- a/packages/cli/src/installers/proofkit-webviewer.ts +++ /dev/null @@ -1,133 +0,0 @@ -import path from "node:path"; -import type { OttoAPIKey } from "@proofkit/fmdapi"; -import chalk from "chalk"; -import dotenv from "dotenv"; -import { getLayouts } from "~/cli/fmdapi.js"; -import { state } from "~/state.js"; -import { readSettings } from "~/utils/parseSettings.js"; -import { type FmAddonInspection, getFmAddonInstallInstructions, inspectFmAddon } from "./install-fm-addon.js"; - -export interface WebViewerAddonStatus { - hasRequiredLayouts?: boolean; - inspection: FmAddonInspection; -} - -export async function checkForWebViewerLayouts(projectDir = state.projectDir): Promise { - const settings = readSettings(projectDir); - const inspection = await inspectFmAddon({ addonName: "wv" }); - - const dataSource = settings.dataSources - .filter((s: { type: string }) => s.type === "fm") - .find((s: { name: string; type: string }) => s.name === "filemaker") as - | { - type: "fm"; - name: string; - envNames: { database: string; server: string; apiKey: string }; - } - | undefined; - - if (!dataSource) { - return { inspection }; - } - if (settings.envFile) { - dotenv.config({ - path: path.join(projectDir, settings.envFile), - }); - } - const dataApiKey = process.env[dataSource.envNames.apiKey] as OttoAPIKey | undefined; - const fmFile = process.env[dataSource.envNames.database]; - const server = process.env[dataSource.envNames.server]; - - if (!(dataApiKey && fmFile && server)) { - return { inspection }; - } - - const existingLayouts = await getLayouts({ - dataApiKey, - fmFile, - server, - }); - const webviewerLayouts = ["ProofKitWV"]; - - const allWebViewerLayoutsExist = webviewerLayouts.every((layout) => - existingLayouts.some((l: string) => l === layout), - ); - - return { - hasRequiredLayouts: allWebViewerLayoutsExist, - inspection, - }; -} - -export function getWebViewerAddonMessages({ hasRequiredLayouts, inspection }: WebViewerAddonStatus): { - info: string[]; - warn: string[]; - nextSteps: string[]; -} { - const messages = { - info: [] as string[], - warn: [] as string[], - nextSteps: [] as string[], - }; - - if (hasRequiredLayouts) { - messages.info.push("Successfully detected all required layouts for ProofKit Web Viewer in your FileMaker file."); - } - - if (inspection.status === "installed-outdated") { - const versionSuffix = - inspection.installedVersion && inspection.latestVersion - ? ` Local version: ${inspection.installedVersion}. Latest version: ${inspection.latestVersion}.` - : ""; - messages.warn.push( - `New ProofKit Web Viewer add-on available. Run \`${inspection.installCommand}\` to download and open it.${versionSuffix}`, - ); - messages.nextSteps.push(inspection.installCommand); - } - - if (inspection.status === "unknown" && inspection.reason === "unsupported-platform") { - messages.warn.push("Could not inspect the local ProofKit Web Viewer add-on on this platform."); - } - - if (hasRequiredLayouts === false) { - const instructions = getFmAddonInstallInstructions("wv"); - messages.warn.push( - "ProofKit Web Viewer layouts were not detected in your FileMaker file. The add-on may not be installed in the file yet.", - ); - if (inspection.status === "missing") { - messages.warn.push( - `Local ProofKit Web Viewer add-on file was not found. Run \`${inspection.installCommand}\` to download and open it.`, - ); - messages.nextSteps.push(inspection.installCommand); - } - if (inspection.status === "unknown" && inspection.reason !== "unsupported-platform") { - messages.warn.push( - "Could not determine the local ProofKit Web Viewer add-on version. Reinstall it explicitly if you need the latest local files.", - ); - messages.nextSteps.push(inspection.installCommand); - } - messages.info.push( - chalk.bgYellow(" ACTION REQUIRED: ") + - ` Install or update the ProofKit Web Viewer add-on in your FileMaker file. ${chalk.dim(`(Learn more: ${instructions.docsUrl})`)}`, - ); - for (const step of instructions.steps) { - messages.info.push(step); - } - } - - return messages; -} - -export async function ensureWebViewerAddonInstalled() { - const status = await checkForWebViewerLayouts(); - const messages = getWebViewerAddonMessages(status); - - for (const message of messages.warn) { - console.log(chalk.yellow(message)); - } - for (const message of messages.info) { - console.log(message); - } - - return status; -} diff --git a/packages/cli/src/installers/react-email.ts b/packages/cli/src/installers/react-email.ts deleted file mode 100644 index 9e59d2f6..00000000 --- a/packages/cli/src/installers/react-email.ts +++ /dev/null @@ -1,209 +0,0 @@ -import path from "node:path"; -import chalk from "chalk"; -import fs from "fs-extra"; -import type { Project } from "ts-morph"; -import type { PackageJson } from "type-fest"; -import * as p from "~/cli/prompts.js"; - -import { abortIfCancel } from "~/cli/utils.js"; -import { PKG_ROOT } from "~/consts.js"; -import { installDependencies } from "~/helpers/installDependencies.js"; -import { isNonInteractiveMode, state } from "~/state.js"; -import { addPackageDependency } from "~/utils/addPackageDependency.js"; -import { addToEnv } from "~/utils/addToEnvs.js"; -import { logger } from "~/utils/logger.js"; -import { getSettings, setSettings } from "~/utils/parseSettings.js"; -import { formatAndSaveSourceFiles, getNewProject } from "~/utils/ts-morph.js"; - -export async function installReactEmail({ - ...args -}: { - project?: Project; - noInstall?: boolean; - installServerFiles?: boolean; -}) { - const projectDir = state.projectDir; - - // Exit early if already installed - const settings = getSettings(); - if (settings.ui === "shadcn") { - return false; - } - if (settings.reactEmail) { - return false; - } - - // Ensure emails directory exists - fs.ensureDirSync(path.join(projectDir, "src/emails")); - addPackageDependency({ - dependencies: ["@react-email/components", "@react-email/render"], - devMode: false, - projectDir, - }); - addPackageDependency({ - dependencies: ["react-email", "@react-email/preview-server"], - devMode: true, - projectDir, - }); - - // add a script to package.json - const pkgJson = fs.readJSONSync(path.join(projectDir, "package.json")) as PackageJson; - if (!pkgJson.scripts) { - pkgJson.scripts = {}; - } - pkgJson.scripts["email:preview"] = "email dev --port 3010 --dir=src/emails"; - fs.writeJSONSync(path.join(projectDir, "package.json"), pkgJson, { - spaces: 2, - }); - - const project = args.project ?? getNewProject(projectDir); - - if (args.installServerFiles) { - const emailProvider = state.emailProvider; - if (emailProvider === "plunk") { - await installPlunk({ project }); - } else if (emailProvider === "resend") { - await installResend({ project }); - } else { - await fs.copy( - path.join(PKG_ROOT, "template/extras/emailProviders/none/email.tsx"), - path.join(projectDir, "src/server/auth/email.tsx"), - ); - } - } - - // Copy base email template(s) into src/emails for preview and reuse - await fs.copy( - path.join(PKG_ROOT, "template/extras/emailTemplates/generic.tsx"), - path.join(projectDir, "src/emails/generic.tsx"), - ); - if (args.installServerFiles) { - await fs.copy( - path.join(PKG_ROOT, "template/extras/emailTemplates/auth-code.tsx"), - path.join(projectDir, "src/emails/auth-code.tsx"), - ); - } - - if (!args.project) { - await formatAndSaveSourceFiles(project); - } - - // Mark as installed - setSettings({ - ...settings, - reactEmail: true, - reactEmailServer: Boolean(args.installServerFiles) || settings.reactEmailServer, - }); - - // Install dependencies unless explicitly skipped - if (!args.noInstall) { - await installDependencies({ projectDir }); - } - return true; -} - -export async function installPlunk({ project }: { project?: Project }) { - const projectDir = state.projectDir; - addPackageDependency({ - dependencies: ["@plunk/node"], - devMode: false, - projectDir, - }); - - let apiKey: string; - if (typeof state.apiKey === "string") { - apiKey = state.apiKey; - } else if (isNonInteractiveMode()) { - apiKey = ""; - } else { - apiKey = abortIfCancel( - await p.text({ - message: `Enter your Plunk API key\n${chalk.dim( - "Enter your Secret API Key from https://app.useplunk.com/settings/api", - )}`, - }), - ); - } - - if (!apiKey) { - logger.warn("You will need to add your Plunk API key to the .env file manually for your app to run."); - } - - console.log(""); - - await addToEnv({ - projectDir, - project, - envs: [ - { - name: "PLUNK_API_KEY", - zodValue: `z.string().startsWith("sk_")`, - type: "server", - defaultValue: apiKey, - }, - ], - }); - - await fs.copy( - path.join(PKG_ROOT, "template/extras/emailProviders/plunk/service.ts"), - path.join(projectDir, "src/server/services/plunk.ts"), - ); - - await fs.copy( - path.join(PKG_ROOT, "template/extras/emailProviders/plunk/email.tsx"), - path.join(projectDir, "src/server/auth/email.tsx"), - ); -} - -export async function installResend({ project }: { project?: Project }) { - const projectDir = state.projectDir; - addPackageDependency({ - dependencies: ["resend"], - devMode: false, - projectDir, - }); - - let apiKey: string; - if (typeof state.apiKey === "string") { - apiKey = state.apiKey; - } else if (isNonInteractiveMode()) { - apiKey = ""; - } else { - apiKey = abortIfCancel( - await p.text({ - message: `Enter your Resend API key\n${chalk.dim( - `Only "Sending Access" permission required: https://resend.com/api-keys`, - )}`, - }), - ); - } - - if (!apiKey) { - logger.warn("You will need to add your Resend API key to the .env file manually for your app to run."); - } - - console.log(""); - - await addToEnv({ - projectDir, - project, - envs: [ - { - name: "RESEND_API_KEY", - zodValue: `z.string().startsWith("re_")`, - type: "server", - defaultValue: apiKey, - }, - ], - }); - - await fs.copy( - path.join(PKG_ROOT, "template/extras/emailProviders/resend/service.ts"), - path.join(projectDir, "src/server/services/resend.ts"), - ); - - await fs.copy( - path.join(PKG_ROOT, "template/extras/emailProviders/resend/email.tsx"), - path.join(projectDir, "src/server/auth/email.tsx"), - ); -} diff --git a/packages/cli/src/package-versions.ts b/packages/cli/src/package-versions.ts deleted file mode 100644 index ed5f3a8f..00000000 --- a/packages/cli/src/package-versions.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const CLI_VERSION: string = "2.2.2"; -export const FMDAPI_VERSION = "5.1.2" as const; -export const BETTER_AUTH_VERSION = "0.4.1" as const; -export const WEBVIEWER_VERSION = "3.1.0" as const; -export const TYPEGEN_VERSION = "1.1.3" as const; diff --git a/packages/cli/src/services/live.ts b/packages/cli/src/services/live.ts deleted file mode 100644 index e0d8621d..00000000 --- a/packages/cli/src/services/live.ts +++ /dev/null @@ -1,844 +0,0 @@ -import { randomUUID } from "node:crypto"; -import path from "node:path"; -import type { Effect as Fx } from "effect"; -import { Effect, Layer } from "effect"; -import { execa } from "execa"; -import fs from "fs-extra"; -import { TEMPLATE_ROOT } from "~/consts.js"; -import { - CliContext, - type CliContextValue, - CodegenService, - ConsoleService, - type FileMakerBootstrapArtifacts, - FileMakerService, - FileSystemService, - GitService, - type OttoApiKeyInfo, - type OttoFileInfo, - PackageManagerService, - ProcessService, - PromptService, - SettingsService, - TemplateService, -} from "~/core/context.js"; -import { ExternalCommandError, FileMakerSetupError, FileSystemError, UserCancelledError } from "~/core/errors.js"; -import type { AppType, FileMakerInputs, ProofKitSettings, UIType } from "~/core/types.js"; -import { installFmAddonExplicitly } from "~/installers/install-fm-addon.js"; -import { openBrowser } from "~/utils/browserOpen.js"; -import { deleteJson, getJson, postJson } from "~/utils/http.js"; -import { detectUserPackageManager } from "~/utils/packageManager.js"; -import { createDataSourceEnvNames, updateEnvSchemaFile, updateTypegenConfig } from "~/utils/projectFiles.js"; -import { - confirmPrompt, - spinner as createSpinner, - isCancel, - log, - multiSearchSelectPrompt, - note, - passwordPrompt, - searchSelectPrompt, - selectPrompt, - textPrompt, -} from "~/utils/prompts.js"; - -function unwrap(value: T | symbol): T { - if (isCancel(value)) { - throw new UserCancelledError({ message: "User aborted the operation" }); - } - return value as T; -} - -function normalizeUrl(serverUrl: string) { - if (serverUrl.startsWith("https://")) { - return serverUrl; - } - if (serverUrl.startsWith("http://")) { - return serverUrl.replace("http://", "https://"); - } - return `https://${serverUrl}`; -} - -interface LayoutFolder { - isFolder?: boolean; - name?: string; - folderLayoutNames?: LayoutFolder[]; -} - -function transformLayoutList(layouts: LayoutFolder[]): string[] { - const flatten = (layout: LayoutFolder): string[] => { - if (layout.isFolder === true) { - const folderLayouts = Array.isArray(layout.folderLayoutNames) ? layout.folderLayoutNames : []; - return folderLayouts.flatMap((item) => flatten(item)); - } - return typeof layout.name === "string" ? [layout.name] : []; - }; - - return layouts.flatMap(flatten).sort((left, right) => left.localeCompare(right)); -} - -function withFsError(operation: string, targetPath: string, run: () => Promise) { - return Effect.tryPromise({ - try: run, - catch: (cause) => - new FileSystemError({ - message: `File system ${operation} failed for ${targetPath}.`, - operation, - path: targetPath, - cause, - }), - }); -} - -function withCommandError(command: string, args: string[], cwd: string, run: () => Promise, message?: string) { - return Effect.tryPromise({ - try: run, - catch: (cause) => - new ExternalCommandError({ - message: message ?? `Command failed: ${[command, ...args].join(" ")}`, - command, - args, - cwd, - cause, - }), - }); -} - -function withFileMakerSetupError(message: string, run: () => Promise) { - return Effect.tryPromise({ - try: run, - catch: (cause) => - new FileMakerSetupError({ - message, - cause, - }), - }); -} - -const promptService = { - text: async (options: { message: string; defaultValue?: string; validate?: (value: string) => string | undefined }) => - unwrap( - await textPrompt({ - message: options.message, - defaultValue: options.defaultValue, - validate: options.validate, - }), - ).toString(), - password: async (options: { message: string; validate?: (value: string) => string | undefined }) => - unwrap( - await passwordPrompt({ - message: options.message, - validate: options.validate, - }), - ).toString(), - select: async (options: { - message: string; - options: Array<{ value: T; label: string; hint?: string }>; - }) => - unwrap( - await selectPrompt({ - message: options.message, - options: options.options, - }), - ) as T, - searchSelect: async (options: { - message: string; - emptyMessage?: string; - options: Array<{ - value: T; - label: string; - hint?: string; - keywords?: string[]; - disabled?: boolean | string; - }>; - }) => unwrap(await searchSelectPrompt(options)) as T, - multiSearchSelect: async (options: { - message: string; - options: Array<{ - value: T; - label: string; - hint?: string; - keywords?: string[]; - disabled?: boolean | string; - }>; - required?: boolean; - }) => unwrap(await multiSearchSelectPrompt(options)), - confirm: async (options: { message: string; initialValue?: boolean }) => - unwrap( - await confirmPrompt({ - message: options.message, - initialValue: options.initialValue, - }), - ) as boolean, -}; - -const consoleService = { - info: (message: string) => log.info(message), - warn: (message: string) => log.warn(message), - error: (message: string) => log.error(message), - success: (message: string) => log.success(message), - note: (message: string, title?: string) => note(message, title), -}; - -const fileSystemService = { - exists: (targetPath: string) => withFsError("exists", targetPath, () => fs.pathExists(targetPath)), - readdir: (targetPath: string) => withFsError("readdir", targetPath, () => fs.readdir(targetPath)), - ensureDir: (targetPath: string) => withFsError("ensureDir", targetPath, () => fs.ensureDir(targetPath)), - emptyDir: (targetPath: string) => withFsError("emptyDir", targetPath, () => fs.emptyDir(targetPath)), - copyDir: (from: string, to: string, options?: { overwrite?: boolean }) => - withFsError("copyDir", `${from} -> ${to}`, () => fs.copy(from, to, { overwrite: options?.overwrite ?? true })), - rename: (from: string, to: string) => withFsError("rename", `${from} -> ${to}`, () => fs.rename(from, to)), - remove: (targetPath: string) => withFsError("remove", targetPath, () => fs.remove(targetPath)), - readJson: (targetPath: string) => withFsError("readJson", targetPath, () => fs.readJson(targetPath) as Promise), - writeJson: (targetPath: string, value: unknown) => - withFsError("writeJson", targetPath, () => fs.writeJson(targetPath, value, { spaces: 2 })), - writeFile: (targetPath: string, content: string) => - withFsError("writeFile", targetPath, () => fs.writeFile(targetPath, content, "utf8")), - readFile: (targetPath: string) => withFsError("readFile", targetPath, () => fs.readFile(targetPath, "utf8")), -}; - -const templateService = { - getTemplateDir: (appType: AppType, _ui: UIType) => { - if (appType === "webviewer") { - return path.join(TEMPLATE_ROOT, "vite-wv"); - } - return path.join(TEMPLATE_ROOT, "nextjs-shadcn"); - }, -}; - -const packageManagerService = { - getVersion: (packageManager: string, cwd: string) => { - if (packageManager === "bun") { - return Effect.succeed(undefined); - } - return withCommandError(packageManager, ["-v"], cwd, async () => { - const { stdout } = await execa(packageManager, ["-v"], { cwd }); - return stdout.trim(); - }); - }, -}; - -const processService = { - run: ( - command: string, - args: string[], - options: { - cwd: string; - stdout?: "pipe" | "inherit" | "ignore"; - stderr?: "pipe" | "inherit" | "ignore"; - }, - ) => - withCommandError(command, args, options.cwd, async () => { - const result = await execa(command, args, { - cwd: options.cwd, - stdout: options.stdout ?? "pipe", - stderr: options.stderr ?? "pipe", - }); - return { - stdout: result.stdout ?? "", - stderr: result.stderr ?? "", - }; - }), -}; - -const gitService = { - initialize: (projectDir: string) => - Effect.gen(function* () { - yield* withCommandError("git", ["init"], projectDir, () => execa("git", ["init"], { cwd: projectDir })); - yield* withCommandError("git", ["add", "."], projectDir, () => execa("git", ["add", "."], { cwd: projectDir })); - yield* withCommandError("git", ["commit", "-m", "Initial commit"], projectDir, () => - execa("git", ["commit", "-m", "Initial commit"], { cwd: projectDir }), - ).pipe( - Effect.catchTag("ExternalCommandError", () => - Effect.sync(() => { - consoleService.warn("Git initial commit failed; continuing without commit."); - }), - ), - ); - }), -}; - -const settingsService = { - writeSettings: (projectDir: string, settings: ProofKitSettings) => - withFsError("writeSettings", path.join(projectDir, "proofkit.json"), () => - fs.writeJson(path.join(projectDir, "proofkit.json"), settings, { - spaces: 2, - }), - ), - appendEnvVars: (projectDir: string, vars: Record) => - withFsError("appendEnvVars", path.join(projectDir, ".env"), async () => { - const envPath = path.join(projectDir, ".env"); - const existing = (await fs.pathExists(envPath)) ? await fs.readFile(envPath, "utf8") : ""; - const additions = Object.entries(vars) - .map(([name, value]) => `${name}=${value}`) - .join("\n"); - const nextContent = [existing.trimEnd(), additions].filter(Boolean).join("\n").concat("\n"); - await fs.writeFile(envPath, nextContent, "utf8"); - }), -}; - -function createDataSourceEntry(dataSourceName: string) { - return { - type: "fm" as const, - name: dataSourceName, - envNames: createDataSourceEnvNames(dataSourceName), - }; -} - -function createFileMakerBootstrapArtifacts( - settings: ProofKitSettings, - inputs: FileMakerInputs, - appType: AppType, -): Promise { - const dataSourceEntry = createDataSourceEntry(inputs.dataSourceName); - const nextSettings: ProofKitSettings = { - ...settings, - dataSources: settings.dataSources.some((entry) => entry.name === dataSourceEntry.name) - ? settings.dataSources - : [...settings.dataSources, dataSourceEntry], - }; - - if (inputs.mode === "local-fm-mcp") { - return Promise.resolve({ - settings: nextSettings, - envVars: {}, - envSchemaEntries: [], - typegenConfig: { - mode: inputs.mode, - dataSourceName: inputs.dataSourceName, - fmMcpBaseUrl: inputs.fmMcpBaseUrl, - connectedFileName: inputs.fileName, - layoutName: inputs.layoutName, - schemaName: inputs.schemaName, - appType, - }, - }); - } - - return Promise.resolve({ - settings: nextSettings, - envVars: { - [inputs.envNames.database]: inputs.fileName, - [inputs.envNames.server]: inputs.server, - [inputs.envNames.apiKey]: inputs.dataApiKey, - }, - envSchemaEntries: [ - { - name: inputs.envNames.database, - zodSchema: 'z.string().endsWith(".fmp12")', - defaultValue: inputs.fileName, - }, - { - name: inputs.envNames.server, - zodSchema: "z.string().url()", - defaultValue: inputs.server, - }, - { - name: inputs.envNames.apiKey, - zodSchema: 'z.string().startsWith("dk_")', - defaultValue: inputs.dataApiKey, - }, - ], - typegenConfig: { - mode: inputs.mode, - dataSourceName: inputs.dataSourceName, - envNames: inputs.envNames, - layoutName: inputs.layoutName, - schemaName: inputs.schemaName, - appType, - }, - }); -} - -const fileMakerService = { - detectLocalFmMcp: (baseUrl = process.env.FM_MCP_BASE_URL ?? "http://127.0.0.1:1365") => - Effect.tryPromise({ - try: async () => { - try { - const health = await fetch(`${baseUrl}/health`, { - signal: AbortSignal.timeout(3000), - }); - if (!health.ok) { - return { baseUrl, healthy: false, connectedFiles: [] }; - } - const connectedFiles = await fetch(`${baseUrl}/connectedFiles`, { - signal: AbortSignal.timeout(3000), - }) - .then(async (response) => (response.ok ? ((await response.json()) as unknown) : [])) - .catch(() => []); - return { - baseUrl, - healthy: true, - connectedFiles: Array.isArray(connectedFiles) - ? connectedFiles.filter((item): item is string => typeof item === "string") - : [], - }; - } catch { - return { baseUrl, healthy: false, connectedFiles: [] }; - } - }, - catch: (cause) => - new FileMakerSetupError({ - message: "Unable to detect local ProofKit plugin.", - cause, - }), - }), - authorizeLocalFmMcp: ({ - baseUrl, - fileName, - interactive, - clientName, - clientDescription, - }: { - baseUrl: string; - fileName: string; - interactive: boolean; - clientName: string; - clientDescription: string; - }) => - Effect.tryPromise({ - try: async () => { - if (!interactive) { - throw new Error("interactive authorization disabled"); - } - const sessionToken = `pk_${randomUUID().replaceAll("-", "")}`; - const response = await postJson<{ status?: unknown; error?: unknown }>( - `${baseUrl}/authorizeSession`, - { - sessionId: sessionToken, - fileName, - clientName, - clientDescription, - }, - { timeout: 125_000 }, - ); - if (response.status >= 200 && response.status < 300 && response.data?.status === "approved") { - return { sessionToken }; - } - const status = response.data?.status; - let reason = "authorization failed"; - if (typeof response.data?.error === "string") { - reason = response.data.error; - } else if (status === "rejected") { - reason = "authorization rejected"; - } else if (status === "timeout") { - reason = "authorization timed out"; - } - throw new Error(reason); - }, - catch: (cause) => - new FileMakerSetupError({ - message: `Not authorized to connect to FileMaker file "${fileName}".`, - cause, - }), - }), - installLocalWebViewerAddon: () => - Effect.tryPromise({ - try: async () => { - await installFmAddonExplicitly({ addonName: "wv" }); - }, - catch: (cause) => - new FileMakerSetupError({ - message: "Unable to install local ProofKit Web Viewer add-on files.", - cause, - }), - }).pipe(Effect.asVoid), - validateHostedServerUrl: (serverUrl: string, ottoPort?: number | null) => - Effect.gen(function* () { - const normalizedUrl = normalizeUrl(serverUrl); - const fmsUrl = new URL("/fmws/serverinfo", normalizedUrl).toString(); - const fmsResponse = yield* withFileMakerSetupError( - `Unable to validate FileMaker Server URL: ${normalizedUrl}`, - () => getJson<{ data?: { ServerVersion?: string } }>(fmsUrl), - ); - const serverVersion = fmsResponse.data?.data?.ServerVersion?.split(" ")[0]; - if (!serverVersion) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: `Invalid FileMaker Server URL: ${normalizedUrl}`, - }), - ); - } - - let ottoVersion: string | null = null; - const otto4Response = yield* withFileMakerSetupError("Unable to query OttoFMS version.", () => - getJson<{ response?: { Otto?: { version?: string } } }>(new URL("/otto/api/info", normalizedUrl).toString()), - ).pipe(Effect.catchAll(() => Effect.succeed(undefined))); - ottoVersion = otto4Response?.data?.response?.Otto?.version ?? null; - - if (!ottoVersion) { - const otto3Url = new URL(normalizedUrl); - otto3Url.port = ottoPort ? String(ottoPort) : "3030"; - otto3Url.pathname = "/api/otto/info"; - const otto3Response = yield* withFileMakerSetupError("Unable to query OttoFMS v3 version.", () => - getJson<{ Otto?: { version?: string } }>(otto3Url.toString()), - ).pipe(Effect.catchAll(() => Effect.succeed(undefined))); - ottoVersion = otto3Response?.data?.Otto?.version ?? null; - } - - return { - normalizedUrl: new URL(normalizedUrl).origin, - versions: { - fmsVersion: serverVersion, - ottoVersion, - }, - }; - }), - getOttoFMSToken: ({ url }: { url: URL }) => - Effect.gen(function* () { - const hash = randomUUID().replaceAll("-", "").slice(0, 18); - const loginUrl = new URL(`/otto/wizard/${hash}`, url.origin); - log.info(`If the browser window didn't open automatically, use this Otto login URL:\n${loginUrl.toString()}`); - yield* withFileMakerSetupError("Unable to open OttoFMS login URL.", () => openBrowser(loginUrl.toString())); - - const spin = createSpinner(); - spin.start("Waiting for OttoFMS login"); - - const deadline = Date.now() + 180_000; - while (Date.now() < deadline) { - const response = yield* withFileMakerSetupError("Unable to poll OttoFMS login status.", () => - getJson<{ response?: { token?: string } }>(`${url.origin}/otto/api/cli/checkHash/${hash}`, { - headers: { "Accept-Encoding": "deflate" }, - timeout: 5000, - }), - ).pipe(Effect.catchAll(() => Effect.succeed(undefined))); - const token = response?.data?.response?.token; - if (token) { - spin.stop("Login complete"); - yield* withFileMakerSetupError("Unable to clean up OttoFMS login state.", () => - deleteJson(`${url.origin}/otto/api/cli/checkHash/${hash}`, { - headers: { "Accept-Encoding": "deflate" }, - }), - ).pipe(Effect.catchAll(() => Effect.void)); - return { token }; - } - yield* Effect.promise(() => new Promise((resolve) => setTimeout(resolve, 500))); - } - - spin.stop("Login timed out"); - return yield* Effect.fail( - new FileMakerSetupError({ - message: "OttoFMS login timed out after 3 minutes.", - }), - ); - }), - listFiles: ({ url, token }: { url: URL; token: string }) => - Effect.gen(function* () { - const response = yield* withFileMakerSetupError("Unable to list FileMaker files from OttoFMS.", () => - getJson<{ - response?: { - databases?: Array<{ filename?: string; status?: string }>; - }; - }>(`${url.origin}/otto/fmi/admin/api/v2/databases`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }), - ); - const databases = Array.isArray(response.data?.response?.databases) ? response.data.response.databases : []; - return databases - .filter((database): database is { filename: string; status?: string } => typeof database.filename === "string") - .map( - (database) => - ({ - filename: database.filename, - status: database.status ?? "unknown", - }) satisfies OttoFileInfo, - ); - }), - listAPIKeys: ({ url, token }: { url: URL; token: string }) => - Effect.gen(function* () { - const response = yield* withFileMakerSetupError("Unable to list OttoFMS Data API keys.", () => - getJson<{ response?: { "api-keys"?: Record[] } }>(`${url.origin}/otto/api/api-key`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }), - ); - const apiKeys = Array.isArray(response.data?.response?.["api-keys"]) ? response.data.response["api-keys"] : []; - return apiKeys - .filter( - ( - apiKey, - ): apiKey is { - key: string; - user: string; - database: string; - label: string; - } => - typeof apiKey.key === "string" && - typeof apiKey.user === "string" && - typeof apiKey.database === "string" && - typeof apiKey.label === "string", - ) - .map( - (apiKey) => - ({ - key: apiKey.key, - user: apiKey.user, - database: apiKey.database, - label: apiKey.label, - }) satisfies OttoApiKeyInfo, - ); - }), - createDataAPIKeyWithCredentials: ({ - url, - filename, - username, - password: userPassword, - }: { - url: URL; - filename: string; - username: string; - password: string; - }) => - Effect.gen(function* () { - const response = yield* withFileMakerSetupError(`Unable to create a Data API key for ${filename}.`, () => - postJson<{ response?: { key?: string } }>(`${url.origin}/otto/api/api-key/create-only`, { - database: filename, - label: "For FM Web App", - user: username, - pass: userPassword, - }), - ); - const apiKey = response.data?.response?.key; - if (!apiKey) { - return yield* Effect.fail( - new FileMakerSetupError({ - message: `Failed to create a Data API key for ${filename}.`, - }), - ); - } - return { apiKey }; - }), - startDeployment: ({ payload, url, token }: { payload: unknown; url: URL; token: string }) => - withFileMakerSetupError("Unable to start ProofKit Demo deployment.", () => - postJson<{ response?: { subDeploymentIds?: number[] } }>(`${url.origin}/otto/api/deployment`, payload, { - headers: { - Authorization: `Bearer ${token}`, - }, - }), - ), - getDeploymentStatus: ({ url, token, deploymentId }: { url: URL; token: string; deploymentId: number }) => - withFileMakerSetupError(`Unable to fetch deployment status for ${deploymentId}.`, () => - getJson<{ response?: { status?: string; running?: boolean } }>( - `${url.origin}/otto/api/deployment/${deploymentId}`, - { - headers: { - Authorization: `Bearer ${token}`, - }, - }, - ), - ), - deployDemoFile: ({ url, token, operation }: { url: URL; token: string; operation: "install" | "replace" }) => - Effect.gen(function* () { - const demoFileName = "ProofKitDemo.fmp12"; - const spin = createSpinner(); - spin.start("Deploying ProofKit Demo file"); - - const deploymentPayload = { - scheduled: false, - label: "Install ProofKit Demo", - deployments: [ - { - name: "Install ProofKit Demo", - source: { - type: "url", - url: "https://proofkit.proof.sh/proofkit-demo/manifest.json", - }, - fileOperations: [ - { - target: { - fileName: demoFileName, - }, - operation, - source: { - fileName: demoFileName, - }, - location: { - folder: "default", - subFolder: "", - }, - }, - ], - concurrency: 1, - options: { - closeFilesAfterBuild: false, - keepFilesClosedAfterComplete: false, - transferContainerData: false, - }, - }, - ], - abortRemaining: false, - }; - - const deployment = yield* fileMakerService.startDeployment({ - payload: deploymentPayload, - url, - token, - }); - - const deploymentId = deployment.data?.response?.subDeploymentIds?.[0]; - if (!deploymentId) { - spin.stop("Demo deployment failed"); - return yield* Effect.fail( - new FileMakerSetupError({ - message: "No deployment ID was returned when deploying the demo file.", - }), - ); - } - - const deploymentDeadline = Date.now() + 300_000; - let deploymentCompleted = false; - while (Date.now() < deploymentDeadline) { - yield* Effect.promise(() => new Promise((resolve) => setTimeout(resolve, 2500))); - const status = yield* fileMakerService.getDeploymentStatus({ - url, - token, - deploymentId, - }); - - if (!status.data?.response?.running) { - if (status.data?.response?.status !== "complete") { - spin.stop("Demo deployment failed"); - return yield* Effect.fail( - new FileMakerSetupError({ - message: "ProofKit Demo deployment did not complete successfully.", - }), - ); - } - deploymentCompleted = true; - break; - } - } - - if (!deploymentCompleted) { - spin.stop("Demo deployment timed out"); - return yield* Effect.fail( - new FileMakerSetupError({ - message: "ProofKit Demo deployment timed out after 5 minutes.", - }), - ); - } - - const apiKey = yield* fileMakerService.createDataAPIKeyWithCredentials({ - url, - filename: demoFileName, - username: "admin", - password: "admin", - }); - spin.stop("Demo file deployed"); - return { apiKey: apiKey.apiKey, filename: demoFileName }; - }), - listLayouts: ({ dataApiKey, fmFile, server }: { dataApiKey: string; fmFile: string; server: string }) => - Effect.gen(function* () { - const response = yield* withFileMakerSetupError(`Unable to list layouts for ${fmFile}.`, () => - getJson<{ response?: { layouts?: LayoutFolder[] } }>( - `${server}/otto/fmi/data/vLatest/databases/${encodeURIComponent(fmFile)}/layouts`, - { - headers: { - Authorization: `Bearer ${dataApiKey}`, - }, - }, - ), - ); - const layouts = Array.isArray(response.data?.response?.layouts) ? response.data.response.layouts : []; - return transformLayoutList(layouts); - }), - createFileMakerBootstrapArtifacts: (settings: ProofKitSettings, inputs: FileMakerInputs, appType: AppType) => - withFileMakerSetupError("Unable to prepare FileMaker bootstrap artifacts.", () => - createFileMakerBootstrapArtifacts(settings, inputs, appType), - ), - bootstrap: (projectDir: string, settings: ProofKitSettings, inputs: FileMakerInputs, appType: AppType) => - Effect.gen(function* () { - const artifacts = yield* fileMakerService.createFileMakerBootstrapArtifacts(settings, inputs, appType); - const projectFilesFs = { - exists: (targetPath: string) => Effect.runPromise(fileSystemService.exists(targetPath)), - readFile: (targetPath: string) => Effect.runPromise(fileSystemService.readFile(targetPath)), - writeFile: (targetPath: string, content: string) => - Effect.runPromise(fileSystemService.writeFile(targetPath, content)), - }; - if (Object.keys(artifacts.envVars).length > 0) { - yield* settingsService.appendEnvVars(projectDir, artifacts.envVars); - yield* withFileMakerSetupError("Unable to update env schema for FileMaker bootstrap.", () => - updateEnvSchemaFile(projectFilesFs, projectDir, artifacts.envSchemaEntries), - ); - } - - yield* withFileMakerSetupError("Unable to update typegen config for FileMaker bootstrap.", () => - updateTypegenConfig(projectFilesFs, projectDir, { - appType: artifacts.typegenConfig.appType, - dataSourceName: artifacts.typegenConfig.dataSourceName, - envNames: artifacts.typegenConfig.envNames, - fmMcpBaseUrl: artifacts.typegenConfig.fmMcpBaseUrl, - connectedFileName: artifacts.typegenConfig.connectedFileName, - layoutName: artifacts.typegenConfig.layoutName, - schemaName: artifacts.typegenConfig.schemaName, - }), - ); - - return artifacts.settings; - }), -}; - -const codegenService = { - runInitial: (projectDir: string, packageManager: CliContextValue["packageManager"], proofkitToken?: string) => { - let commandParts: string[]; - if (packageManager === "npm") { - commandParts = ["npm", "run", "typegen"]; - } else if (packageManager === "bun") { - commandParts = ["bun", "run", "typegen"]; - } else { - commandParts = [packageManager, "typegen"]; - } - const command = commandParts[0]; - if (!command) { - return Effect.fail( - new ExternalCommandError({ - message: "Unable to resolve the codegen command", - command: packageManager, - args: commandParts.slice(1), - cwd: projectDir, - }), - ); - } - const args = commandParts.slice(1); - return withCommandError( - command, - args, - projectDir, - async () => { - await execa(command, args, { - cwd: projectDir, - env: proofkitToken ? { FM_MCP_SESSION_ID: proofkitToken } : undefined, - }); - }, - "Initial codegen failed", - ); - }, -}; - -export function makeLiveLayer(options: { cwd: string; debug: boolean; nonInteractive: boolean }) { - const cliContext: CliContextValue = { - cwd: options.cwd, - debug: options.debug, - nonInteractive: options.nonInteractive, - packageManager: detectUserPackageManager(), - }; - - const layer = Layer.mergeAll( - Layer.succeed(CliContext, cliContext), - Layer.succeed(PromptService, promptService), - Layer.succeed(ConsoleService, consoleService), - Layer.succeed(FileSystemService, fileSystemService), - Layer.succeed(TemplateService, templateService), - Layer.succeed(PackageManagerService, packageManagerService), - Layer.succeed(ProcessService, processService), - Layer.succeed(GitService, gitService), - Layer.succeed(SettingsService, settingsService), - Layer.succeed(FileMakerService, fileMakerService), - Layer.succeed(CodegenService, codegenService), - ); - - return (effect: Fx.Effect) => Effect.provide(effect, layer); -} diff --git a/packages/cli/src/state.ts b/packages/cli/src/state.ts deleted file mode 100644 index 1130be82..00000000 --- a/packages/cli/src/state.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { z } from "zod/v4"; - -const schema = z - .object({ - nonInteractive: z.boolean().default(false), - debug: z.boolean().default(false), - localBuild: z.boolean().default(false), - baseCommand: z.enum(["add", "init", "deploy", "upgrade", "remove"]).optional().catch(undefined), - appType: z.enum(["browser", "webviewer"]).optional().catch(undefined), - ui: z.enum(["shadcn", "mantine"]).optional().catch("shadcn"), - projectDir: z.string().default(process.cwd()), - authType: z.enum(["clerk", "fmaddon"]).optional(), - emailProvider: z.enum(["plunk", "resend", "none"]).optional(), - dataSource: z.enum(["filemaker", "none"]).optional(), - }) - .passthrough(); - -type ProgramState = z.infer; -export let state: ProgramState = schema.parse({}); - -export function initProgramState(args: unknown) { - const parsed = schema.safeParse(args); - if (parsed.success) { - const mergedState = { ...state, ...parsed.data }; - state = mergedState; - } -} - -export function isNonInteractiveMode() { - return state.nonInteractive; -} diff --git a/packages/cli/src/upgrades/cursorRules.ts b/packages/cli/src/upgrades/cursorRules.ts deleted file mode 100644 index 1338225f..00000000 --- a/packages/cli/src/upgrades/cursorRules.ts +++ /dev/null @@ -1,41 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; - -import { PKG_ROOT } from "~/consts.js"; -import { state } from "~/state.js"; -import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; - -export async function copyCursorRules() { - const projectDir = state.projectDir; - const extrasDir = path.join(PKG_ROOT, "template/extras"); - const cursorRulesSrcDir = path.join(extrasDir, "_cursor/rules"); - const cursorRulesDestDir = path.join(projectDir, ".cursor/rules"); - - if (!fs.existsSync(cursorRulesSrcDir)) { - return; - } - - const pkgManager = getUserPkgManager(); - await fs.ensureDir(cursorRulesDestDir); - await fs.copy(cursorRulesSrcDir, cursorRulesDestDir); - - // Copy package manager specific rules - const conditionalRulesDir = path.join(extrasDir, "_cursor/conditional-rules"); - - const packageManagerRules = { - pnpm: "pnpm.mdc", - npm: "npm.mdc", - yarn: "yarn.mdc", - }; - - const selectedRule = packageManagerRules[pkgManager as keyof typeof packageManagerRules]; - - if (selectedRule) { - const ruleSrc = path.join(conditionalRulesDir, selectedRule); - const ruleDest = path.join(cursorRulesDestDir, "package-manager.mdc"); - - if (fs.existsSync(ruleSrc)) { - await fs.copy(ruleSrc, ruleDest, { overwrite: true }); - } - } -} diff --git a/packages/cli/src/upgrades/index.ts b/packages/cli/src/upgrades/index.ts deleted file mode 100644 index b72dbc98..00000000 --- a/packages/cli/src/upgrades/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { type appTypes, getSettings, mergeSettings } from "~/utils/parseSettings.js"; -import { copyCursorRules } from "./cursorRules.js"; -import { addShadcn } from "./shadcn.js"; - -interface Upgrade { - key: string; - title: string; - description: string; - appType: (typeof appTypes)[number][]; - function: () => Promise; -} - -const availableUpgrades: Upgrade[] = [ - { - key: "cursorRules", - title: "Upgrade Cursor Rules", - description: "Upgrade the .cursor rules in your project to the latest version.", - appType: ["browser"], - function: copyCursorRules, - }, - { - key: "shadcn", - title: "Add Shadcn", - description: - "Add Shadcn to your project, to support easily adding new components from a variety of component registries.", - appType: ["browser", "webviewer"], - function: addShadcn, - }, -]; - -export type UpgradeKeys = (typeof availableUpgrades)[number]["key"]; - -export function checkForAvailableUpgrades() { - const settings = getSettings(); - if (settings.ui === "shadcn") { - return []; - } - - const appliedUpgrades = settings.appliedUpgrades; - - const neededUpgrades = availableUpgrades.filter( - (upgrade) => !appliedUpgrades.includes(upgrade.key) && upgrade.appType.includes(settings.appType), - ); - - return neededUpgrades.map(({ key, title, description }) => ({ - key, - title, - description, - })); -} - -export async function runAllAvailableUpgrades() { - const upgrades = checkForAvailableUpgrades(); - const settings = getSettings(); - if (settings.ui === "shadcn") { - return; - } - - for (const upgrade of upgrades) { - const upgradeFunction = availableUpgrades.find((u) => u.key === upgrade.key)?.function; - if (upgradeFunction) { - await upgradeFunction(); - const appliedUpgrades = settings.appliedUpgrades; - mergeSettings({ - appliedUpgrades: [...appliedUpgrades, upgrade.key], - }); - } - } -} diff --git a/packages/cli/src/upgrades/shadcn.ts b/packages/cli/src/upgrades/shadcn.ts deleted file mode 100644 index ea10bfd0..00000000 --- a/packages/cli/src/upgrades/shadcn.ts +++ /dev/null @@ -1,55 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; - -import { PKG_ROOT } from "~/consts.js"; -import { installDependencies } from "~/helpers/installDependencies.js"; -import type { AvailableDependencies } from "~/installers/dependencyVersionMap.js"; -import { state } from "~/state.js"; -import { addPackageDependency } from "~/utils/addPackageDependency.js"; - -const BASE_DEPS = [ - "@radix-ui/react-slot", - "@tailwindcss/postcss", - "class-variance-authority", - "clsx", - "lucide-react", - "tailwind-merge", - "tailwindcss", - "tw-animate-css", -] as AvailableDependencies[]; -const BASE_DEV_DEPS = [] as AvailableDependencies[]; - -export async function addShadcn() { - const projectDir = state.projectDir; - - const TEMPLATE_ROOT = path.join(PKG_ROOT, "template/nextjs-shadcn"); - - // 1. Add dependencies - addPackageDependency({ - dependencies: BASE_DEPS, - devMode: false, - projectDir, - }); - addPackageDependency({ - dependencies: BASE_DEV_DEPS, - devMode: true, - projectDir, - }); - - // 2. Copy config and utility files - fs.copySync(path.join(TEMPLATE_ROOT, "components.json"), path.join(projectDir, "components.json")); - fs.copySync(path.join(TEMPLATE_ROOT, "postcss.config.cjs"), path.join(projectDir, "postcss.config.cjs")); - await fs.ensureDir(path.join(projectDir, "src/utils")); - fs.copySync(path.join(TEMPLATE_ROOT, "src/utils/styles.ts"), path.join(projectDir, "src/utils/styles.ts")); - await fs.ensureDir(path.join(projectDir, "src/config/theme")); - fs.copySync( - path.join(TEMPLATE_ROOT, "src/config/theme/globals.css"), - path.join(projectDir, "src/config/theme/globals.css"), - ); - - // 3. Install dependencies - await installDependencies(); - - // 4. Success message - console.log("\nโœ… shadcn/ui + Tailwind v4 upgrade complete!\n"); -} diff --git a/packages/cli/src/utils/addPackageDependency.ts b/packages/cli/src/utils/addPackageDependency.ts deleted file mode 100644 index 304d9f2a..00000000 --- a/packages/cli/src/utils/addPackageDependency.ts +++ /dev/null @@ -1,32 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; -import type { PackageJson } from "type-fest"; - -import { type AvailableDependencies, dependencyVersionMap } from "~/installers/dependencyVersionMap.js"; -import { state } from "~/state.js"; -import { sortPackageJson } from "~/utils/sortPackageJson.js"; - -export const addPackageDependency = (opts: { - dependencies: AvailableDependencies[]; - devMode: boolean; - projectDir?: string; -}) => { - const { dependencies, devMode, projectDir = state.projectDir } = opts; - - const pkgJson = fs.readJSONSync(path.join(projectDir, "package.json")) as PackageJson; - - for (const pkgName of dependencies) { - const version = dependencyVersionMap[pkgName]; - - if (devMode && pkgJson.devDependencies) { - pkgJson.devDependencies[pkgName] = version; - } else if (pkgJson.dependencies) { - pkgJson.dependencies[pkgName] = version; - } - } - const sortedPkgJson = sortPackageJson(pkgJson); - - fs.writeJSONSync(path.join(projectDir, "package.json"), sortedPkgJson, { - spaces: 2, - }); -}; diff --git a/packages/cli/src/utils/addToEnvs.ts b/packages/cli/src/utils/addToEnvs.ts deleted file mode 100644 index 5af7e131..00000000 --- a/packages/cli/src/utils/addToEnvs.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { execSync } from "node:child_process"; -import path from "node:path"; -import fs from "fs-extra"; -import { type Project, SyntaxKind } from "ts-morph"; - -import { findT3EnvFile } from "~/installers/envVars.js"; -import { state } from "~/state.js"; -import { formatAndSaveSourceFiles, getNewProject } from "./ts-morph.js"; - -interface EnvSchema { - name: string; - zodValue: string; - /** This value will be added to the .env file, unless `addToRuntimeEnv` is set to `false`. */ - defaultValue?: string; - type: "server" | "client"; - addToRuntimeEnv?: boolean; -} - -export async function addToEnv({ - projectDir = state.projectDir, - envs, - envFileDescription, - ...args -}: { - projectDir?: string; - project?: Project; - envs: EnvSchema[]; - envFileDescription?: string; -}) { - const envSchemaFile = findT3EnvFile(); - - const project = args.project ?? getNewProject(projectDir); - const schemaFile = project.addSourceFileAtPath(envSchemaFile); - - if (!schemaFile) { - throw new Error("Schema file not found"); - } - - // Find the createEnv call expression - const createEnvCall = schemaFile - .getDescendantsOfKind(SyntaxKind.CallExpression) - .find((callExpr) => callExpr.getExpression().getText() === "createEnv"); - - if (!createEnvCall) { - throw new Error( - "Could not find createEnv call in schema file. Make sure you have a valid env.ts file with createEnv setup.", - ); - } - - // Get the server object property - const opts = createEnvCall.getArguments()[0]; - if (!opts) { - throw new Error("createEnv call is missing options argument"); - } - - const serverProperty = opts - .getDescendantsOfKind(SyntaxKind.PropertyAssignment) - .find((prop) => prop.getName() === "server") - ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression); - - const clientProperty = opts - .getDescendantsOfKind(SyntaxKind.PropertyAssignment) - .find((prop) => prop.getName() === "client") - ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression); - - const runtimeEnvProperty = opts - .getDescendantsOfKind(SyntaxKind.PropertyAssignment) - .find((prop) => prop.getName() === "experimental__runtimeEnv") - ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression); - - const serverEnvs = envs.filter((env) => env.type === "server"); - const clientEnvs = envs.filter((env) => env.type === "client"); - - for (const env of serverEnvs) { - serverProperty?.addPropertyAssignment({ - name: env.name, - initializer: env.zodValue, - }); - } - - for (const env of clientEnvs) { - clientProperty?.addPropertyAssignment({ - name: env.name, - initializer: env.zodValue, - }); - - runtimeEnvProperty?.addPropertyAssignment({ - name: env.name, - initializer: `process.env.${env.name}`, - }); - } - - const envsString = envs - .filter((env) => env.addToRuntimeEnv ?? true) - .map((env) => `${env.name}=${env.defaultValue ?? ""}`) - .join("\n"); - - const dotEnvFile = path.join(projectDir, ".env"); - - // Only handle .env file if it already exists - if (fs.existsSync(dotEnvFile)) { - const currentFile = fs.readFileSync(dotEnvFile, "utf-8"); - - // Ensure .env is in .gitignore using command line - const gitIgnoreFile = path.join(projectDir, ".gitignore"); - try { - let gitIgnoreContent = ""; - if (fs.existsSync(gitIgnoreFile)) { - gitIgnoreContent = fs.readFileSync(gitIgnoreFile, "utf-8"); - } - - if (!gitIgnoreContent.includes(".env")) { - execSync(`echo ".env" >> "${gitIgnoreFile}"`, { cwd: projectDir }); - } - } catch (_error) { - // Silently ignore gitignore errors - } - - const newContent = `${currentFile} -${envFileDescription ? `# ${envFileDescription}\n${envsString}` : envsString} - `; - - fs.writeFileSync(dotEnvFile, newContent); - } - - if (!args.project) { - await formatAndSaveSourceFiles(project); - } - - return schemaFile; -} diff --git a/packages/cli/src/utils/browserOpen.ts b/packages/cli/src/utils/browserOpen.ts deleted file mode 100644 index 5580d98b..00000000 --- a/packages/cli/src/utils/browserOpen.ts +++ /dev/null @@ -1,11 +0,0 @@ -import open from "open"; - -export async function openBrowser(url: string): Promise { - try { - await open(url); - } catch { - // Ignore open failures and let the user copy the URL manually. - } -} - -export const openExternal: (url: string) => Promise = openBrowser; diff --git a/packages/cli/src/utils/formatting.ts b/packages/cli/src/utils/formatting.ts deleted file mode 100644 index 2c491be8..00000000 --- a/packages/cli/src/utils/formatting.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { execa } from "execa"; -import type { Project } from "ts-morph"; - -import { state } from "~/state.js"; - -/** - * Formats all source files in a ts-morph Project using ultracite and saves the changes. - * @param project The ts-morph Project containing the files to format - */ -export async function formatAndSaveSourceFiles(project: Project) { - await project.save(); // save files first - try { - // Run ultracite fix on the project directory - await execa("npx", ["ultracite", "fix", "."], { - cwd: state.projectDir, - }); - } catch (error) { - if (state.debug) { - console.log("Error formatting files with ultracite"); - console.error(error); - } - // Continue even if formatting fails - } -} diff --git a/packages/cli/src/utils/getProofKitVersion.ts b/packages/cli/src/utils/getProofKitVersion.ts deleted file mode 100644 index 29134255..00000000 --- a/packages/cli/src/utils/getProofKitVersion.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - BETTER_AUTH_VERSION, - CLI_VERSION, - FMDAPI_VERSION, - TYPEGEN_VERSION, - WEBVIEWER_VERSION, -} from "~/package-versions.js"; - -export const getVersion = () => { - return CLI_VERSION; -}; - -export const getFmdapiVersion = () => { - return FMDAPI_VERSION; -}; - -export const getNodeMajorVersion = () => { - const defaultVersion = "22"; - try { - return process.versions.node.split(".")[0] ?? defaultVersion; - } catch { - return defaultVersion; - } -}; - -export const getProofkitBetterAuthVersion = () => { - return BETTER_AUTH_VERSION; -}; - -export const getProofkitWebviewerVersion = () => { - return WEBVIEWER_VERSION; -}; - -export const getTypegenVersion = () => { - return TYPEGEN_VERSION; -}; - -export const getProofkitDependencyVersion = (version: string) => `^${version}`; diff --git a/packages/cli/src/utils/getUserPkgManager.ts b/packages/cli/src/utils/getUserPkgManager.ts deleted file mode 100644 index d2e3afdc..00000000 --- a/packages/cli/src/utils/getUserPkgManager.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type PackageManager = "npm" | "pnpm" | "yarn" | "bun"; - -export const getUserPkgManager: () => PackageManager = () => { - // This environment variable is set by npm and yarn but pnpm seems less consistent - const userAgent = process.env.npm_config_user_agent; - - if (userAgent) { - if (userAgent.startsWith("yarn")) { - return "yarn"; - } - if (userAgent.startsWith("pnpm")) { - return "pnpm"; - } - if (userAgent.startsWith("bun")) { - return "bun"; - } - return "npm"; - } - // If no user agent is set, assume pnpm - return "pnpm"; -}; diff --git a/packages/cli/src/utils/http.ts b/packages/cli/src/utils/http.ts deleted file mode 100644 index 87e88bb9..00000000 --- a/packages/cli/src/utils/http.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { readFile } from "node:fs/promises"; -import https from "node:https"; -import axios from "axios"; - -function createHttpsAgent() { - return new https.Agent({ - rejectUnauthorized: process.env.PROOFKIT_ALLOW_INSECURE_TLS !== "1", - }); -} - -export async function getJson(url: string, options?: { headers?: Record; timeout?: number }) { - const response = await axios.get(url, { - headers: options?.headers, - httpsAgent: createHttpsAgent(), - timeout: options?.timeout ?? 10_000, - validateStatus: null, - }); - return response; -} - -export async function postJson( - url: string, - data: unknown, - options?: { headers?: Record; timeout?: number }, -) { - const response = await axios.post(url, data, { - headers: options?.headers, - httpsAgent: createHttpsAgent(), - timeout: options?.timeout ?? 10_000, - validateStatus: null, - }); - return response; -} - -export async function deleteJson(url: string, options?: { headers?: Record; timeout?: number }) { - const response = await axios.delete(url, { - headers: options?.headers, - httpsAgent: createHttpsAgent(), - timeout: options?.timeout ?? 10_000, - validateStatus: null, - }); - return response; -} - -export async function requestJson( - url: string | URL, - options?: { - method?: "GET" | "POST" | "DELETE"; - headers?: Record; - body?: Record; - timeoutMs?: number; - }, -) { - if (url.toString().startsWith("file://")) { - return { - status: 200, - data: JSON.parse(await readFile(new URL(url), "utf8")) as T, - }; - } - - const response = await axios.request({ - url: url.toString(), - method: options?.method ?? "GET", - data: options?.body, - headers: options?.headers, - httpsAgent: createHttpsAgent(), - timeout: options?.timeoutMs ?? 10_000, - }); - return response; -} - -export async function requestText( - url: string | URL, - options?: { - method?: "GET" | "POST" | "DELETE"; - headers?: Record; - timeoutMs?: number; - }, -) { - const response = await axios.request({ - url: url.toString(), - method: options?.method ?? "GET", - headers: options?.headers, - httpsAgent: createHttpsAgent(), - timeout: options?.timeoutMs ?? 10_000, - responseType: "text", - validateStatus: null, - }); - return { - status: response.status, - data: response.data, - }; -} - -export async function requestArrayBuffer( - url: string | URL, - options?: { - method?: "GET"; - headers?: Record; - timeoutMs?: number; - }, -) { - if (url.toString().startsWith("file://")) { - return { - status: 200, - data: await readFile(new URL(url)), - }; - } - - const response = await axios.request({ - url: url.toString(), - method: options?.method ?? "GET", - headers: options?.headers, - httpsAgent: createHttpsAgent(), - timeout: options?.timeoutMs ?? 30_000, - responseType: "arraybuffer", - validateStatus: null, - }); - return { - status: response.status, - data: Buffer.from(response.data), - }; -} diff --git a/packages/cli/src/utils/isTTYError.ts b/packages/cli/src/utils/isTTYError.ts deleted file mode 100644 index ccf602ed..00000000 --- a/packages/cli/src/utils/isTTYError.ts +++ /dev/null @@ -1 +0,0 @@ -export class IsTTYError extends Error {} diff --git a/packages/cli/src/utils/logger.ts b/packages/cli/src/utils/logger.ts deleted file mode 100644 index 3ddb9775..00000000 --- a/packages/cli/src/utils/logger.ts +++ /dev/null @@ -1,19 +0,0 @@ -import chalk from "chalk"; - -export const logger = { - error(...args: unknown[]) { - console.log(chalk.red(...args)); - }, - warn(...args: unknown[]) { - console.log(chalk.yellow(...args)); - }, - info(...args: unknown[]) { - console.log(chalk.cyan(...args)); - }, - success(...args: unknown[]) { - console.log(chalk.green(...args)); - }, - dim(...args: unknown[]) { - console.log(chalk.dim(...args)); - }, -}; diff --git a/packages/cli/src/utils/nonInteractive.ts b/packages/cli/src/utils/nonInteractive.ts deleted file mode 100644 index a619af17..00000000 --- a/packages/cli/src/utils/nonInteractive.ts +++ /dev/null @@ -1,35 +0,0 @@ -const NON_INTERACTIVE_ENV_VARS = [ - "CI", - "GITHUB_ACTIONS", - "CODEX", - "OPENAI_CODEX", - "CLAUDE_CODE", - "JENKINS_URL", - "BUILDKITE", -] as const; - -export function detectNonInteractiveTerminal(options?: { - stdinIsTTY?: boolean; - stdoutIsTTY?: boolean; - env?: NodeJS.ProcessEnv; -}): boolean { - const env = options?.env ?? process.env; - const hasTTY = options?.stdinIsTTY === true && options?.stdoutIsTTY === true; - const hasNonInteractiveEnv = NON_INTERACTIVE_ENV_VARS.some((name) => Boolean(env[name])); - const hasDumbTerm = env.TERM === "dumb"; - return !hasTTY || hasNonInteractiveEnv || hasDumbTerm; -} - -export function resolveNonInteractiveMode(options?: { - CI?: boolean; - nonInteractive?: boolean; - stdinIsTTY?: boolean; - stdoutIsTTY?: boolean; - env?: NodeJS.ProcessEnv; -}) { - if (options?.nonInteractive === true || options?.CI === true) { - return true; - } - - return detectNonInteractiveTerminal(options); -} diff --git a/packages/cli/src/utils/packageManager.ts b/packages/cli/src/utils/packageManager.ts deleted file mode 100644 index 15402c66..00000000 --- a/packages/cli/src/utils/packageManager.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type PackageManager = "npm" | "pnpm" | "yarn" | "bun"; - -export function detectUserPackageManager(): PackageManager { - const userAgent = process.env.npm_config_user_agent; - - if (userAgent) { - if (userAgent.startsWith("yarn")) { - return "yarn"; - } - if (userAgent.startsWith("pnpm")) { - return "pnpm"; - } - if (userAgent.startsWith("bun")) { - return "bun"; - } - return "npm"; - } - - return "npm"; -} diff --git a/packages/cli/src/utils/parseNameAndPath.ts b/packages/cli/src/utils/parseNameAndPath.ts deleted file mode 100644 index 0e6fae65..00000000 --- a/packages/cli/src/utils/parseNameAndPath.ts +++ /dev/null @@ -1,46 +0,0 @@ -import pathModule from "node:path"; - -import { removeTrailingSlash } from "./removeTrailingSlash.js"; - -const whitespaceRegex = /\s+/g; - -/** - * Parses the appName and its path from the user input. - * - * Returns a tuple of of `[appName, path]`, where `appName` is the name put in the "package.json" - * file and `path` is the path to the directory where the app will be created. - * - * If `appName` is ".", the name of the directory will be used instead. Handles the case where the - * input includes a scoped package name in which case that is being parsed as the name, but not - * included as the path. - * - * For example: - * - * - dir/@mono/app => ["@mono/app", "dir/app"] - * - dir/app => ["app", "dir/app"] - */ -export const parseNameAndPath = (rawInput: string) => { - const input = removeTrailingSlash(rawInput); - const paths = input.split("/"); - const normalizedPaths = [...paths]; - const lastPathIndex = normalizedPaths.length - 1; - - let appName = (normalizedPaths.at(-1) ?? "").replace(whitespaceRegex, "-").toLowerCase(); - normalizedPaths[lastPathIndex] = appName; - - // If the user ran `npx proofkit .` or similar, the appName should be the current directory - if (appName === ".") { - const parsedCwd = pathModule.resolve(process.cwd()); - appName = pathModule.basename(parsedCwd).replace(whitespaceRegex, "-").toLowerCase(); - } - - // If the first part is a @, it's a scoped package - const indexOfDelimiter = normalizedPaths.findIndex((p) => p.startsWith("@")); - if (indexOfDelimiter !== -1) { - appName = normalizedPaths.slice(indexOfDelimiter).join("/"); - } - - const path = normalizedPaths.filter((p) => !p.startsWith("@")).join("/"); - - return [appName, path] as const; -}; diff --git a/packages/cli/src/utils/parseSettings.ts b/packages/cli/src/utils/parseSettings.ts deleted file mode 100644 index 4424826a..00000000 --- a/packages/cli/src/utils/parseSettings.ts +++ /dev/null @@ -1,162 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; -import { z } from "zod/v4"; - -import { state } from "~/state.js"; - -const authSchema = z - .discriminatedUnion("type", [ - z.object({ - type: z.literal("clerk"), - }), - z.object({ - type: z.literal("next-auth"), - }), - z.object({ - type: z.literal("proofkit").transform(() => "fmaddon"), - }), - z.object({ - type: z.literal("fmaddon"), - }), - z.object({ - type: z.literal("better-auth"), - }), - z.object({ - type: z.literal("none"), - }), - ]) - .default({ type: "none" }); - -export const envNamesSchema = z.object({ - database: z.string().default("FM_DATABASE"), - server: z.string().default("FM_SERVER"), - apiKey: z.string().default("OTTO_API_KEY"), -}); -export const dataSourceSchema = z.discriminatedUnion("type", [ - z.object({ - type: z.literal("fm"), - name: z.string(), - envNames: envNamesSchema, - }), - z.object({ - type: z.literal("supabase"), - name: z.string(), - }), -]); -export type DataSource = z.infer; - -export const appTypes = ["browser", "webviewer"] as const; - -export const uiTypes = ["shadcn", "mantine"] as const; -export type Ui = (typeof uiTypes)[number]; - -const settingsSchema = z.discriminatedUnion("ui", [ - z.object({ - ui: z.literal("mantine"), - appType: z.enum(appTypes).default("browser"), - auth: authSchema, - envFile: z.string().optional(), - dataSources: z.array(dataSourceSchema).default([]), - tanstackQuery: z.boolean().catch(false), - replacedMainPage: z.boolean().catch(false), - // Whether React Email scaffolding has been installed - reactEmail: z.boolean().catch(false), - // Whether provider-specific server email sender files have been installed - reactEmailServer: z.boolean().catch(false), - appliedUpgrades: z.array(z.string()).default([]), - registryUrl: z.url().optional(), - registryTemplates: z.array(z.string()).default([]), - }), - z.object({ - ui: z.literal("shadcn"), - appType: z.enum(appTypes).default("browser"), - envFile: z.string().optional(), - dataSources: z.array(dataSourceSchema).default([]), - replacedMainPage: z.boolean().catch(false), - registryUrl: z.url().optional(), - registryTemplates: z.array(z.string()).default([]), - }), -]); - -export const defaultSettings = settingsSchema.parse({ - auth: { type: "none" }, - ui: "shadcn", - appType: "browser", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], -}); - -let settings: Settings | undefined; -function parseSettingsFile(settingsPath: string) { - // Check if the settings file exists before trying to read it - if (!fs.existsSync(settingsPath)) { - throw new Error(`ProofKit settings file not found at: ${settingsPath}`); - } - - let settingsFile: unknown = fs.readJSONSync(settingsPath); - - if (typeof settingsFile === "object" && settingsFile !== null && !("ui" in settingsFile)) { - settingsFile = { ...settingsFile, ui: "mantine" }; - } - - const parsed = settingsSchema.parse(settingsFile); - return parsed; -} - -export const getSettings = () => { - if (settings) { - return settings; - } - - const settingsPath = path.join(state.projectDir, "proofkit.json"); - const parsed = parseSettingsFile(settingsPath); - - state.appType = parsed.appType; - settings = parsed; - return parsed; -}; - -export function readSettings(projectDir = state.projectDir) { - return parseSettingsFile(path.join(projectDir, "proofkit.json")); -} - -export type Settings = z.infer; - -export function mergeSettings(_settings: Partial) { - const settings = getSettings(); - const merged = { ...settings, ..._settings }; - const validated = settingsSchema.parse(merged); - setSettings(validated); -} - -export function setSettings(_settings: Settings) { - fs.writeJSONSync(path.join(state.projectDir, "proofkit.json"), _settings, { - spaces: 2, - }); - settings = _settings; - return settings; -} - -/** - * Validates and sets the envFile in settings only if the file exists. - * Used during stealth initialization to avoid setting non-existent env files. - */ -export function validateAndSetEnvFile(envFileName = ".env") { - const settings = getSettings(); - const envFilePath = path.join(state.projectDir, envFileName); - - if (fs.existsSync(envFilePath)) { - const updatedSettings = { ...settings, envFile: envFileName }; - setSettings(updatedSettings); - return envFileName; - } - - // If no env file exists, ensure envFile is undefined in settings - if (settings.envFile) { - const { envFile, ...settingsWithoutEnvFile } = settings; - setSettings(settingsWithoutEnvFile as Settings); - } - - return undefined; -} diff --git a/packages/cli/src/utils/projectFiles.ts b/packages/cli/src/utils/projectFiles.ts deleted file mode 100644 index cb5fbee6..00000000 --- a/packages/cli/src/utils/projectFiles.ts +++ /dev/null @@ -1,350 +0,0 @@ -import { readFileSync } from "node:fs"; -import path from "node:path"; -import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser/lib/esm/main.js"; -import { PKG_ROOT } from "~/consts.js"; -import type { FileMakerEnvNames } from "~/core/types.js"; -import { getVersion } from "~/utils/getProofKitVersion.js"; -import type { PackageManager } from "~/utils/packageManager.js"; - -const commonFileMakerLayoutPrefixes = ["API_", "API ", "dapi_", "dapi"]; -const TRAILING_SLASH_REGEX = /[^/]$/; -const WHITESPACE_REGEX = /\s/; -const DEFAULT_FM_MCP_BASE_URL = "http://127.0.0.1:1365"; -const textFileExtensions = new Set([ - ".ts", - ".tsx", - ".js", - ".jsx", - ".json", - ".jsonc", - ".md", - ".css", - ".scss", - ".html", - ".mjs", - ".cjs", -]); - -export function getDefaultSchemaName(layoutName: string) { - let schemaName = layoutName.replace(/[-\s]/g, "_"); - for (const prefix of commonFileMakerLayoutPrefixes) { - if (schemaName.startsWith(prefix)) { - schemaName = schemaName.replace(prefix, ""); - } - } - return schemaName; -} - -export function createDataSourceEnvNames(dataSourceName: string): FileMakerEnvNames { - if (dataSourceName === "filemaker") { - return { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }; - } - - const upperName = dataSourceName.toUpperCase(); - return { - database: `${upperName}_FM_DATABASE`, - server: `${upperName}_FM_SERVER`, - apiKey: `${upperName}_OTTO_API_KEY`, - }; -} - -export function formatPackageManagerCommand(packageManager: PackageManager, command: string) { - return ["npm", "bun"].includes(packageManager) ? `${packageManager} run ${command}` : `${packageManager} ${command}`; -} - -export function parseCommandString(command: string): string[] { - const tokens: string[] = []; - let current = ""; - let quote: "'" | '"' | undefined; - let escaping = false; - - for (const char of command) { - if (escaping) { - current += char; - escaping = false; - continue; - } - - if (char === "\\") { - escaping = true; - continue; - } - - if (quote) { - if (char === quote) { - quote = undefined; - } else { - current += char; - } - continue; - } - - if (char === "'" || char === '"') { - quote = char; - continue; - } - - if (WHITESPACE_REGEX.test(char)) { - if (current) { - tokens.push(current); - current = ""; - } - continue; - } - - current += char; - } - - if (escaping) { - current += "\\"; - } - - if (current) { - tokens.push(current); - } - - return tokens; -} - -export function getTemplatePackageCommand(packageManager: PackageManager) { - if (packageManager === "npm") { - return "npm run"; - } - return packageManager; -} - -export function getTemplatePackageExecuteCommand(packageManager: PackageManager) { - if (packageManager === "npm") { - return "npx"; - } - if (packageManager === "pnpm") { - return "pnpx"; - } - if (packageManager === "bun") { - return "bunx"; - } - return `${packageManager} dlx`; -} - -export function normalizeImportAlias(importAlias: string) { - return importAlias.replace(/\*/g, "").replace(TRAILING_SLASH_REGEX, "$&/"); -} - -export async function replaceTextInFiles( - fs: { - readdir: (path: string) => Promise; - readFile: (path: string) => Promise; - writeFile: (path: string, content: string) => Promise; - }, - rootDir: string, - searchValue: string, - replaceValue: string, -) { - const entries = await fs.readdir(rootDir); - for (const entry of entries) { - const fullPath = path.join(rootDir, entry); - const childEntries = await fs.readdir(fullPath).catch((error: unknown) => { - const code = - typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" - ? error.code - : undefined; - - if (code === "ENOTDIR") { - return undefined; - } - - throw error; - }); - if (childEntries) { - await replaceTextInFiles(fs, fullPath, searchValue, replaceValue); - continue; - } - - const extension = path.extname(entry); - if (!textFileExtensions.has(extension)) { - continue; - } - - const content = await fs.readFile(fullPath).catch(() => undefined); - if (!content?.includes(searchValue)) { - continue; - } - - await fs.writeFile(fullPath, content.replaceAll(searchValue, replaceValue)); - } -} - -export async function updateEnvSchemaFile( - fs: { - exists: (path: string) => Promise; - readFile: (path: string) => Promise; - writeFile: (path: string, content: string) => Promise; - }, - projectDir: string, - envEntries: Array<{ name: string; zodSchema: string }>, -) { - const envFilePath = path.join(projectDir, "src/lib/env.ts"); - if (!(await fs.exists(envFilePath))) { - return; - } - - let content = await fs.readFile(envFilePath); - const marker = " server: {"; - const markerIndex = content.indexOf(marker); - if (markerIndex === -1) { - return; - } - - const insertIndex = content.indexOf(" },", markerIndex); - if (insertIndex === -1) { - return; - } - - const additions = envEntries - .filter((entry) => !content.includes(`${entry.name}:`)) - .map((entry) => ` ${entry.name}: ${entry.zodSchema},`) - .join("\n"); - - if (!additions) { - return; - } - - content = `${content.slice(0, insertIndex)}${additions}\n${content.slice(insertIndex)}`; - await fs.writeFile(envFilePath, content); -} - -interface TypegenFileContent { - $schema?: string; - config: Record[] | Record; -} - -export async function updateTypegenConfig( - fs: { - exists: (path: string) => Promise; - readFile: (path: string) => Promise; - writeFile: (path: string, content: string) => Promise; - }, - projectDir: string, - options: { - appType: "browser" | "webviewer"; - dataSourceName: string; - envNames?: FileMakerEnvNames; - fmMcpBaseUrl?: string; - connectedFileName?: string; - layoutName?: string; - schemaName?: string; - }, -) { - const configPath = path.join(projectDir, "proofkit-typegen.config.jsonc"); - const dsPath = `./src/config/schemas/${options.dataSourceName}`; - const nextDataSource: Record = { - type: "fmdapi", - layouts: [], - path: dsPath, - clearOldFiles: true, - clientSuffix: "Layout", - }; - - if (options.envNames) { - nextDataSource.envNames = { - server: options.envNames.server, - db: options.envNames.database, - auth: { apiKey: options.envNames.apiKey }, - }; - } - - if (options.appType === "webviewer") { - nextDataSource.webviewerScriptName = "ExecuteDataApi"; - } - - if (options.fmMcpBaseUrl || options.connectedFileName) { - nextDataSource.fmMcp = { - enabled: true, - ...(options.fmMcpBaseUrl && options.fmMcpBaseUrl !== DEFAULT_FM_MCP_BASE_URL - ? { baseUrl: options.fmMcpBaseUrl } - : {}), - ...(options.connectedFileName ? { connectedFileName: options.connectedFileName } : {}), - }; - } - - const layout = - options.layoutName && options.schemaName - ? { - layoutName: options.layoutName, - schemaName: options.schemaName, - valueLists: "allowEmpty", - } - : undefined; - - if (layout) { - nextDataSource.layouts = [layout]; - } - - if (!(await fs.exists(configPath))) { - const nextContent: TypegenFileContent = { - $schema: "https://proofkit.proof.sh/typegen-config-schema.json", - config: [nextDataSource], - }; - await fs.writeFile(configPath, `${JSON.stringify(nextContent, null, 2)}\n`); - return; - } - - const original = await fs.readFile(configPath); - const parsed = parseJsonc(original) as TypegenFileContent; - const configArray = Array.isArray(parsed.config) ? parsed.config : [parsed.config]; - const existingIndex = configArray.findIndex((entry) => entry.path === dsPath); - - if (existingIndex === -1) { - configArray.push(nextDataSource); - } else { - const existing = (configArray[existingIndex] ?? {}) as Record; - const existingLayouts = Array.isArray(existing.layouts) ? existing.layouts : []; - let nextLayouts = existingLayouts; - if (layout && !existingLayouts.some((item) => item?.layoutName === layout.layoutName)) { - nextLayouts = [...existingLayouts, layout]; - } - configArray[existingIndex] = { - ...existing, - ...nextDataSource, - layouts: nextLayouts, - }; - } - - const nextConfig = Array.isArray(parsed.config) ? configArray : (configArray[0] ?? nextDataSource); - const edits = modify(original, ["config"], nextConfig, { - formattingOptions: { - insertSpaces: true, - tabSize: 2, - eol: "\n", - }, - }); - await fs.writeFile(configPath, applyEdits(original, edits)); -} - -export function getScaffoldVersion() { - const generatedVersion = getVersion(); - if (generatedVersion && generatedVersion !== "0.0.0-private") { - return generatedVersion; - } - - const candidates = [path.resolve(PKG_ROOT, "package.json"), path.resolve(PKG_ROOT, "../cli/package.json")]; - - for (const candidate of candidates) { - try { - const packageJson = JSON.parse(readFileSync(candidate, "utf8")) as { - version?: string; - }; - if (packageJson.version && packageJson.version !== "0.0.0-private") { - return packageJson.version; - } - } catch { - // ignore and continue searching - } - } - - return "0.0.0-private"; -} diff --git a/packages/cli/src/utils/projectName.ts b/packages/cli/src/utils/projectName.ts deleted file mode 100644 index 2c4ee329..00000000 --- a/packages/cli/src/utils/projectName.ts +++ /dev/null @@ -1,63 +0,0 @@ -import path from "node:path"; - -const TRAILING_SLASHES_REGEX = /\/+$/; -const PATH_SEPARATOR_REGEX = /\\/g; -const WHITESPACE_REGEX = /\s+/g; -const VALID_APP_NAME_REGEX = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/; - -function normalizeProjectName(value: string) { - return value.replace(PATH_SEPARATOR_REGEX, "/"); -} - -function trimTrailingSlashes(value: string) { - return normalizeProjectName(value).replace(TRAILING_SLASHES_REGEX, ""); -} - -function normalizeProjectNameForPackage(value: string) { - return trimTrailingSlashes(value).replace(WHITESPACE_REGEX, "-").toLowerCase(); -} - -export function parseNameAndPath(projectName: string): [scopedAppName: string, appDir: string] { - const normalizedProjectName = trimTrailingSlashes(projectName); - const segments = normalizedProjectName.split("/"); - const hasScopedPackage = (segments.at(-2) ?? "").startsWith("@"); - const packageSegmentCount = hasScopedPackage ? 2 : 1; - const leadingSegments = segments.slice(0, -packageSegmentCount); - const packageSegments = segments.slice(-packageSegmentCount); - const normalizedPackageSegments = packageSegments.map(normalizeProjectNameForPackage); - let scopedAppName = normalizedPackageSegments.join("/"); - let appDirPackageSegments = normalizedPackageSegments; - - if (scopedAppName === ".") { - scopedAppName = normalizeProjectNameForPackage(path.basename(path.resolve(process.cwd()))); - appDirPackageSegments = packageSegments; - } - - const appDir = [...leadingSegments, ...appDirPackageSegments].join("/"); - - return [scopedAppName, appDir]; -} - -export function validateAppName(projectName: string) { - const normalized = normalizeProjectNameForPackage(projectName); - if (normalized === ".") { - const currentDirName = path.basename(path.resolve(process.cwd())); - return VALID_APP_NAME_REGEX.test(currentDirName.replace(WHITESPACE_REGEX, "-").toLowerCase()) - ? undefined - : "Name must consist of only lowercase alphanumeric characters, '-', and '_'"; - } - - const segments = normalized.split("/"); - const scopeIndex = segments.findIndex((segment) => segment.startsWith("@")); - let scopedAppName = segments.at(-1); - - if (scopeIndex !== -1) { - scopedAppName = segments.slice(scopeIndex).join("/"); - } - - if (VALID_APP_NAME_REGEX.test(scopedAppName ?? "")) { - return; - } - - return "Name must consist of only lowercase alphanumeric characters, '-', and '_'"; -} diff --git a/packages/cli/src/utils/prompts.ts b/packages/cli/src/utils/prompts.ts deleted file mode 100644 index 5f977b7c..00000000 --- a/packages/cli/src/utils/prompts.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { - intro as clackIntro, - isCancel as clackIsCancel, - log as clackLog, - note as clackNote, - outro as clackOutro, - spinner as clackSpinner, -} from "@clack/prompts"; -import { - checkbox as inquirerCheckbox, - confirm as inquirerConfirm, - input as inquirerInput, - password as inquirerPassword, - search as inquirerSearch, - select as inquirerSelect, -} from "@inquirer/prompts"; - -const CANCEL_SYMBOL = Symbol.for("@proofkit/new/prompt-cancelled"); - -export const intro = clackIntro; -export const log = clackLog; -export const note = clackNote; -export const outro = clackOutro; -export const spinner = clackSpinner; - -function isPromptCancel(error: unknown) { - return error instanceof Error && error.name === "ExitPromptError"; -} - -function withCancelSentinel(fn: () => Promise): Promise { - return fn().catch((error: unknown) => { - if (isPromptCancel(error)) { - return CANCEL_SYMBOL; - } - throw error; - }); -} - -export function isCancel(value: unknown): value is symbol { - return value === CANCEL_SYMBOL || clackIsCancel(value); -} - -export interface PromptOption { - value: T; - label: string; - hint?: string; - disabled?: boolean | string; -} - -export interface SearchPromptOption extends PromptOption { - keywords?: readonly string[]; -} - -function normalizeValidate( - validate: ((value: string) => string | undefined) | undefined, -): ((value: string) => string | boolean) | undefined { - if (!validate) { - return undefined; - } - - return (value: string) => validate(value) ?? true; -} - -function matchesSearch(option: SearchPromptOption, query: string) { - const haystack = [option.label, option.hint ?? "", ...(option.keywords ?? [])].join(" ").toLowerCase(); - return haystack.includes(query.trim().toLowerCase()); -} - -function normalizeDisabledMessage(value: boolean | string | undefined) { - if (typeof value === "string") { - return value; - } - return value ? true : undefined; -} - -export function filterSearchOptions( - options: readonly SearchPromptOption[], - query: string | undefined, -) { - const term = query?.trim(); - if (!term) { - return options; - } - - return options.filter((option) => matchesSearch(option, term)); -} - -export function textPrompt(options: { - message: string; - defaultValue?: string; - validate?: (value: string) => string | undefined; -}) { - return withCancelSentinel(() => - inquirerInput({ - message: options.message, - default: options.defaultValue, - validate: normalizeValidate(options.validate), - }), - ); -} - -export function passwordPrompt(options: { message: string; validate?: (value: string) => string | undefined }) { - return withCancelSentinel(() => - inquirerPassword({ - message: options.message, - validate: normalizeValidate(options.validate), - }), - ); -} - -export function confirmPrompt(options: { message: string; initialValue?: boolean }) { - return withCancelSentinel(() => - inquirerConfirm({ - message: options.message, - default: options.initialValue, - }), - ); -} - -export function selectPrompt(options: { message: string; options: PromptOption[] }) { - return withCancelSentinel(() => - inquirerSelect({ - message: options.message, - pageSize: 10, - choices: options.options.map((option) => ({ - value: option.value, - name: option.label, - description: option.hint, - disabled: normalizeDisabledMessage(option.disabled), - })), - }), - ); -} - -export function searchSelectPrompt(options: { - message: string; - emptyMessage?: string; - options: SearchPromptOption[]; -}) { - return withCancelSentinel(() => - inquirerSearch({ - message: options.message, - pageSize: 10, - source: (input) => { - const filtered = filterSearchOptions(options.options, input); - if (filtered.length === 0) { - return [ - { - value: "__no_matches__" as T, - name: options.emptyMessage ?? "No matches found. Keep typing to refine your search.", - disabled: options.emptyMessage ?? "No matches found", - }, - ]; - } - - return filtered.map((option) => ({ - value: option.value, - name: option.label, - description: option.hint, - disabled: normalizeDisabledMessage(option.disabled), - })); - }, - }), - ); -} - -export function multiSearchSelectPrompt(options: { - message: string; - options: SearchPromptOption[]; - required?: boolean; -}) { - return withCancelSentinel(() => - inquirerCheckbox({ - message: options.message, - pageSize: 10, - required: options.required, - choices: options.options.map((option) => ({ - value: option.value, - name: option.label, - description: option.hint, - disabled: normalizeDisabledMessage(option.disabled), - })), - }), - ); -} diff --git a/packages/cli/src/utils/proofkitReleaseChannel.ts b/packages/cli/src/utils/proofkitReleaseChannel.ts deleted file mode 100644 index aa7ecf17..00000000 --- a/packages/cli/src/utils/proofkitReleaseChannel.ts +++ /dev/null @@ -1,93 +0,0 @@ -import path from "node:path"; -import fs from "fs-extra"; -import semver from "semver"; - -import { - getFmdapiVersion, - getProofkitBetterAuthVersion, - getProofkitWebviewerVersion, - getTypegenVersion, - getVersion, -} from "~/utils/getProofKitVersion.js"; - -export type ProofkitReleaseTag = "latest" | "beta"; - -interface ChangesetPreState { - mode?: string; - tag?: string; -} - -function findRepoRootWithChangeset(startDir: string): string | null { - let currentDir = path.resolve(startDir); - const { root } = path.parse(currentDir); - - while (currentDir !== root) { - if (fs.existsSync(path.join(currentDir, ".changeset"))) { - return currentDir; - } - currentDir = path.dirname(currentDir); - } - - return null; -} - -function readChangesetPreState(startDir = process.cwd()): ChangesetPreState | null { - const repoRoot = findRepoRootWithChangeset(startDir); - if (!repoRoot) { - return null; - } - - const prePath = path.join(repoRoot, ".changeset", "pre.json"); - if (!fs.existsSync(prePath)) { - return null; - } - - try { - return fs.readJSONSync(prePath) as ChangesetPreState; - } catch { - return null; - } -} - -export function hasAnyPrereleaseVersion(versionCandidates?: Array) { - if (versionCandidates) { - return versionCandidates.some((version) => { - if (!version) { - return false; - } - return semver.valid(version) && semver.prerelease(version); - }); - } - - const readVersion = (getter: () => string) => { - try { - return getter(); - } catch { - return null; - } - }; - - const proofkitVersions = [ - readVersion(getVersion), - readVersion(getFmdapiVersion), - readVersion(getProofkitWebviewerVersion), - readVersion(getTypegenVersion), - readVersion(getProofkitBetterAuthVersion), - ].filter((version): version is string => Boolean(version)); - - return proofkitVersions.some((version) => semver.valid(version) && semver.prerelease(version)); -} - -export function getProofkitReleaseTag(startDir = process.cwd()): ProofkitReleaseTag { - const preState = readChangesetPreState(startDir); - - if (preState?.mode === "pre" && preState.tag === "beta") { - return "beta"; - } - - if (hasAnyPrereleaseVersion()) { - return "beta"; - } - - return "latest"; -} diff --git a/packages/cli/src/utils/removeTrailingSlash.ts b/packages/cli/src/utils/removeTrailingSlash.ts deleted file mode 100644 index 051c3322..00000000 --- a/packages/cli/src/utils/removeTrailingSlash.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const removeTrailingSlash = (input: string) => { - if (input.length > 1 && input.endsWith("/")) { - return input.slice(0, -1); - } - return input; -}; diff --git a/packages/cli/src/utils/renderTitle.ts b/packages/cli/src/utils/renderTitle.ts deleted file mode 100644 index 1a6a8898..00000000 --- a/packages/cli/src/utils/renderTitle.ts +++ /dev/null @@ -1,18 +0,0 @@ -import gradient from "gradient-string"; -import { getTitleText } from "~/consts.js"; -import { detectUserPackageManager } from "~/utils/packageManager.js"; - -const proofTheme = { - purple: "#89216B", - lightPurple: "#D15ABB", - orange: "#FF595E", -}; - -export const proofGradient = gradient(Object.values(proofTheme)); -export function renderTitle(version = "0.0.0-private") { - const pkgManager = detectUserPackageManager(); - if (pkgManager === "yarn" || pkgManager === "pnpm") { - console.log(""); - } - console.log(proofGradient.multiline(getTitleText(version))); -} diff --git a/packages/cli/src/utils/renderVersionWarning.ts b/packages/cli/src/utils/renderVersionWarning.ts deleted file mode 100644 index fd046831..00000000 --- a/packages/cli/src/utils/renderVersionWarning.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { execSync } from "node:child_process"; -import https from "node:https"; -import chalk from "chalk"; -import * as semver from "semver"; -import * as p from "~/cli/prompts.js"; - -import { cliName, npmName } from "~/consts.js"; -import { getVersion } from "./getProofKitVersion.js"; -import { getUserPkgManager } from "./getUserPkgManager.js"; -import { logger } from "./logger.js"; - -export const renderVersionWarning = (npmVersion: string) => { - const currentVersion = getVersion(); - - // Check if current version is a pre-release (beta, alpha, etc.) - if (semver.prerelease(currentVersion)) { - logger.warn(` You are using a pre-release version of ${cliName}.`); - logger.warn(" Please report any bugs you encounter."); - } else if (semver.valid(currentVersion) && semver.valid(npmVersion) && semver.lt(currentVersion, npmVersion)) { - logger.warn(` You are using an outdated version of ${cliName}.`); - logger.warn(" Your version:", `${currentVersion}.`, "Latest version in the npm registry:", npmVersion); - logger.warn(" Please run the CLI with @latest to get the latest updates."); - } - console.log(""); -}; - -/** - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the LICENSE file in the root - * directory of this source tree. - * https://github.com/facebook/create-react-app/blob/main/packages/create-react-app/LICENSE - */ -interface DistTagsBody { - latest: string; -} - -function checkForLatestVersion(): Promise { - return new Promise((resolve, reject) => { - https - .get("https://registry.npmjs.org/-/package/@proofkit/cli/dist-tags", (res) => { - if (res.statusCode === 200) { - let body = ""; - res.on("data", (data) => { - body += data; - }); - res.on("end", () => { - resolve((JSON.parse(body) as DistTagsBody).latest); - }); - } else { - reject(); - } - }) - .on("error", () => { - // logger.error("Unable to check for latest version."); - reject(); - }); - }); -} - -export const getNpmVersion = async () => - // `fetch` to the registry is faster than `npm view` so we try that first - checkForLatestVersion().catch(() => { - try { - return execSync("npm view proofkit version").toString().trim(); - } catch { - return null; - } - }); - -export const checkAndRenderVersionWarning = async () => { - const npmVersion = await getNpmVersion(); - const currentVersion = getVersion(); - - // Only show warning if current version is valid, npm version is valid, and current is actually older - if (npmVersion && semver.valid(currentVersion) && semver.valid(npmVersion) && semver.lt(currentVersion, npmVersion)) { - const pkgManager = getUserPkgManager(); - p.log.warn( - `${chalk.yellow( - `You are using an outdated version of ${cliName}.`, - )} Your version: ${currentVersion}. Latest version: ${npmVersion}. - Run ${chalk.magenta.bold(`${pkgManager} install ${npmName}@latest`)} to get the latest updates.`, - ); - } - return { npmVersion, currentVersion }; -}; diff --git a/packages/cli/src/utils/sortPackageJson.ts b/packages/cli/src/utils/sortPackageJson.ts deleted file mode 100644 index f2f45980..00000000 --- a/packages/cli/src/utils/sortPackageJson.ts +++ /dev/null @@ -1,86 +0,0 @@ -const ROOT_KEY_ORDER = [ - "name", - "version", - "private", - "description", - "keywords", - "homepage", - "bugs", - "repository", - "license", - "author", - "contributors", - "funding", - "type", - "packageManager", - "devEngines", - "engines", - "bin", - "exports", - "main", - "module", - "types", - "files", - "scripts", - "dependencies", - "devDependencies", - "peerDependencies", - "peerDependenciesMeta", - "optionalDependencies", - "bundledDependencies", - "resolutions", - "overrides", - "pnpm", - "lint-staged", -] as const; - -const ROOT_KEY_POSITION = new Map(ROOT_KEY_ORDER.map((key, index) => [key, index])); -const SORTED_OBJECT_KEYS = new Set([ - "scripts", - "dependencies", - "devDependencies", - "peerDependencies", - "peerDependenciesMeta", - "optionalDependencies", - "bundledDependencies", - "resolutions", - "overrides", -]); - -function compareRootKeys(left: string, right: string) { - const leftIndex = ROOT_KEY_POSITION.get(left); - const rightIndex = ROOT_KEY_POSITION.get(right); - - if (leftIndex !== undefined && rightIndex !== undefined) { - return leftIndex - rightIndex; - } - - if (leftIndex !== undefined) { - return -1; - } - - if (rightIndex !== undefined) { - return 1; - } - - return left.localeCompare(right); -} - -function sortRecord(value: Record) { - return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right))); -} - -export function sortPackageJson>(packageJson: T): T { - const sortedEntries = Object.entries(packageJson).sort(([left], [right]) => compareRootKeys(left, right)); - const sortedPackageJson = Object.fromEntries( - sortedEntries.map(([key, value]) => { - if (SORTED_OBJECT_KEYS.has(key) && value && typeof value === "object" && !Array.isArray(value)) { - return [key, sortRecord(value as Record)]; - } - - return [key, value]; - }), - ); - - return sortedPackageJson as T; -} diff --git a/packages/cli/src/utils/ts-morph.ts b/packages/cli/src/utils/ts-morph.ts deleted file mode 100644 index 92d58954..00000000 --- a/packages/cli/src/utils/ts-morph.ts +++ /dev/null @@ -1,25 +0,0 @@ -import path from "node:path"; -import { Project, type ReturnStatement, SyntaxKind } from "ts-morph"; - -export { formatAndSaveSourceFiles } from "./formatting.js"; - -export function ensureReturnStatementIsWrappedInFragment(returnStatement: ReturnStatement | undefined) { - const expression = - returnStatement?.getExpressionIfKind(SyntaxKind.ParenthesizedExpression)?.getExpression() ?? - returnStatement?.getExpression(); - - if (expression?.isKind(SyntaxKind.JsxFragment)) { - return returnStatement; - } - - returnStatement?.replaceWithText(`return <>${expression};`); - return returnStatement; -} - -export function getNewProject(projectDir?: string) { - const project = new Project({ - tsConfigFilePath: path.join(projectDir ?? process.cwd(), "tsconfig.json"), - }); - - return project; -} diff --git a/packages/cli/src/utils/validateAppName.ts b/packages/cli/src/utils/validateAppName.ts deleted file mode 100644 index e2173d94..00000000 --- a/packages/cli/src/utils/validateAppName.ts +++ /dev/null @@ -1,29 +0,0 @@ -import path from "node:path"; - -import { removeTrailingSlash } from "./removeTrailingSlash.js"; - -const validationRegExp = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/; -const whitespaceRegex = /\s+/g; - -//Validate a string against allowed package.json names -export const validateAppName = (rawInput: string) => { - const input = removeTrailingSlash(rawInput).replace(whitespaceRegex, "-").toLowerCase(); - const paths = input.split("/"); - - // If the first part is a @, it's a scoped package - const indexOfDelimiter = paths.findIndex((p) => p.startsWith("@")); - - let appName = paths.at(-1); - if (paths.findIndex((p) => p.startsWith("@")) !== -1) { - appName = paths.slice(indexOfDelimiter).join("/"); - } - - if (input === ".") { - appName = path.basename(path.resolve(process.cwd())).replace(whitespaceRegex, "-").toLowerCase(); - } - - if (validationRegExp.test(appName ?? "")) { - return; - } - return "Name must consist of only lowercase alphanumeric characters, '-', and '_'"; -}; diff --git a/packages/cli/src/utils/validateImportAlias.ts b/packages/cli/src/utils/validateImportAlias.ts deleted file mode 100644 index bd33ca61..00000000 --- a/packages/cli/src/utils/validateImportAlias.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const validateImportAlias = (input: string) => { - if (input.startsWith(".") || input.startsWith("/")) { - return "Import alias can't start with '.' or '/'"; - } - return; -}; diff --git a/packages/cli/src/utils/versioning.ts b/packages/cli/src/utils/versioning.ts deleted file mode 100644 index 55aab322..00000000 --- a/packages/cli/src/utils/versioning.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function getNodeMajorVersion() { - return process.versions.node.split(".")[0] ?? "22"; -} diff --git a/packages/cli/template/extras/_cursor/conditional-rules/nextjs-framework.mdc b/packages/cli/template/extras/_cursor/conditional-rules/nextjs-framework.mdc deleted file mode 100644 index 5ce7a9e0..00000000 --- a/packages/cli/template/extras/_cursor/conditional-rules/nextjs-framework.mdc +++ /dev/null @@ -1,51 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -# Next.js Framework Configuration - -This rule documents the Next.js framework setup and conventions used in this project. - - -name: nextjs_framework -description: Documents Next.js framework setup, routing conventions, and best practices -filters: - - type: file_extension - pattern: "\\.(ts|tsx)$" - - type: directory - pattern: "src/(app|components)/" - -conventions: - routing: - - App Router is used (not Pages Router) - - Routes are defined in src/app directory - - Layout components should be named layout.tsx - - Page components should be named page.tsx - - Loading states should be in loading.tsx - - Error boundaries should be in error.tsx - - components: - - React Server Components (RSC) are default - - Client components must be marked with "use client" - - Components live in src/components/ - - Shared layouts in src/components/layouts/ - - UI components in src/components/ui/ - - data_fetching: - - Server components fetch data directly - - Client components use React Query - - API routes defined in src/app/api/ - - Server actions used for mutations - -frameworks: - next: "15.1.7" - react: "19.0.0-rc" - typescript: "^5" - mantine: "^7.17.0" - tanstack_query: "^5.59.0" - -metadata: - priority: high - version: 1.0 - \ No newline at end of file diff --git a/packages/cli/template/extras/_cursor/conditional-rules/npm.mdc b/packages/cli/template/extras/_cursor/conditional-rules/npm.mdc deleted file mode 100644 index 3b030fa5..00000000 --- a/packages/cli/template/extras/_cursor/conditional-rules/npm.mdc +++ /dev/null @@ -1,60 +0,0 @@ ---- -description: | - This rule documents the package manager configuration and usage. It should be included when: - 1. Installing dependencies - 2. Running scripts - 3. Managing project packages - 4. Running development commands - 5. Executing build or test operations -globs: - - "package.json" - - "package-lock.json" - - ".npmrc" -alwaysApply: true ---- -# Package Manager Configuration - -This rule documents the package manager setup and usage requirements. - - -name: package_manager -description: Documents package manager configuration and usage requirements - -configuration: - name: "npm" - version: "latest" - commands: - install: "npm install" - build: "npm run build" - dev: "npm run dev" - typegen: "npm run typegen" - typecheck: "npm run tsc" - notes: "Always use npm instead of yarn or pnpm for consistency" - dev_server_guidelines: - - "Never relaunch the dev server command if it may already be running" - - "Use npm run dev only when explicitly needed to start the server for the first time" - - "For code changes, just save the files and the server will automatically reload" - -examples: - - description: "Installing dependencies" - correct: "npm install" - incorrect: - - "pnpm install" - - "yarn install" - - - description: "Running scripts" - correct: "npm run script-name" - incorrect: - - "pnpm run script-name" - - "yarn script-name" - - - description: "Adding dependencies" - correct: "npm install package-name" - incorrect: - - "pnpm add package-name" - - "yarn add package-name" - -metadata: - priority: high - version: 1.0 - \ No newline at end of file diff --git a/packages/cli/template/extras/_cursor/conditional-rules/pnpm.mdc b/packages/cli/template/extras/_cursor/conditional-rules/pnpm.mdc deleted file mode 100644 index d25da047..00000000 --- a/packages/cli/template/extras/_cursor/conditional-rules/pnpm.mdc +++ /dev/null @@ -1,65 +0,0 @@ ---- -description: | -globs: -alwaysApply: true ---- ---- -description: | - This rule documents the package manager configuration and usage. It should be included when: - 1. Installing dependencies - 2. Running scripts - 3. Managing project packages - 4. Running development commands - 5. Executing build or test operations -globs: - - "package.json" - - "pnpm-lock.yaml" - - ".npmrc" -alwaysApply: true ---- -# Package Manager Configuration - -This rule documents the package manager setup and usage requirements. - - -name: package_manager -description: Documents package manager configuration and usage requirements - -configuration: - name: "pnpm" - version: "latest" - commands: - install: "pnpm install" - build: "pnpm build" - dev: "pnpm dev" - typegen: "pnpm typegen" - typecheck: "pnpm tsc" - notes: "Always use pnpm instead of npm or yarn for consistency" - dev_server_guidelines: - - "Never relaunch the dev server command if it may already be running" - - "Use pnpm dev only when explicitly needed to start the server for the first time" - - "For code changes, just save the files and the server will automatically reload" - -examples: - - description: "Installing dependencies" - correct: "pnpm install" - incorrect: - - "npm install" - - "yarn install" - - - description: "Running scripts" - correct: "pnpm run script-name" - incorrect: - - "npm run script-name" - - "yarn script-name" - - - description: "Adding dependencies" - correct: "pnpm add package-name" - incorrect: - - "npm install package-name" - - "yarn add package-name" - -metadata: - priority: high - version: 1.0 - \ No newline at end of file diff --git a/packages/cli/template/extras/_cursor/conditional-rules/yarn.mdc b/packages/cli/template/extras/_cursor/conditional-rules/yarn.mdc deleted file mode 100644 index 5672e80e..00000000 --- a/packages/cli/template/extras/_cursor/conditional-rules/yarn.mdc +++ /dev/null @@ -1,60 +0,0 @@ ---- -description: | - This rule documents the package manager configuration and usage. It should be included when: - 1. Installing dependencies - 2. Running scripts - 3. Managing project packages - 4. Running development commands - 5. Executing build or test operations -globs: - - "package.json" - - "yarn.lock" - - ".yarnrc" -alwaysApply: true ---- -# Package Manager Configuration - -This rule documents the package manager setup and usage requirements. - - -name: package_manager -description: Documents package manager configuration and usage requirements - -configuration: - name: "yarn" - version: "latest" - commands: - install: "yarn install" - build: "yarn build" - dev: "yarn dev" - typegen: "yarn typegen" - typecheck: "yarn tsc" - notes: "Always use yarn instead of npm or pnpm for consistency" - dev_server_guidelines: - - "Never relaunch the dev server command if it may already be running" - - "Use yarn dev only when explicitly needed to start the server for the first time" - - "For code changes, just save the files and the server will automatically reload" - -examples: - - description: "Installing dependencies" - correct: "yarn install" - incorrect: - - "npm install" - - "pnpm install" - - - description: "Running scripts" - correct: "yarn script-name" - incorrect: - - "npm run script-name" - - "pnpm run script-name" - - - description: "Adding dependencies" - correct: "yarn add package-name" - incorrect: - - "npm install package-name" - - "pnpm add package-name" - -metadata: - priority: high - version: 1.0 - \ No newline at end of file diff --git a/packages/cli/template/extras/_cursor/rules/cursor-rules.mdc b/packages/cli/template/extras/_cursor/rules/cursor-rules.mdc deleted file mode 100644 index 061da499..00000000 --- a/packages/cli/template/extras/_cursor/rules/cursor-rules.mdc +++ /dev/null @@ -1,88 +0,0 @@ ---- -description: | - This rule documents how to manage and organize Cursor rules. It should be included when: - 1. Creating or modifying Cursor rules - 2. Organizing documentation for the codebase - 3. Setting up new development patterns - 4. Adding project-wide conventions - 5. Managing rule file locations - 6. Updating rule descriptions or globs - 7. Working with .cursor directory structure -globs: - - ".cursor/rules/*.mdc" - - ".cursor/config/*.json" - - ".cursor/settings/*.json" -alwaysApply: true ---- -# Cursor Rules Location - -Rules for placing and organizing Cursor rule files in the repository. - - -name: cursor_rules_location -description: Standards for placing Cursor rule files in the correct directory -filters: - # Match any .mdc files - - type: file_extension - pattern: "\\.mdc$" - # Match files that look like Cursor rules - - type: content - pattern: "(?s).*?" - # Match file creation events - - type: event - pattern: "file_create" - -actions: - - type: reject - conditions: - - pattern: "^(?!\\.\\/\\.cursor\\/rules\\/.*\\.mdc$)" - message: "Cursor rule files (.mdc) must be placed in the .cursor/rules directory" - - - type: suggest - message: | - When creating Cursor rules: - - 1. Always place rule files in PROJECT_ROOT/.cursor/rules/: - ``` - .cursor/rules/ - โ”œโ”€โ”€ your-rule-name.mdc - โ”œโ”€โ”€ another-rule.mdc - โ””โ”€โ”€ ... - ``` - - 2. Follow the naming convention: - - Use kebab-case for filenames - - Always use .mdc extension - - Make names descriptive of the rule's purpose - - 3. Directory structure: - ``` - PROJECT_ROOT/ - โ”œโ”€โ”€ .cursor/ - โ”‚ โ””โ”€โ”€ rules/ - โ”‚ โ”œโ”€โ”€ your-rule-name.mdc - โ”‚ โ””โ”€โ”€ ... - โ””โ”€โ”€ ... - ``` - - 4. Never place rule files: - - In the project root - - In subdirectories outside .cursor/rules - - In any other location - - Inside of the cursor-rules.mdc file - -examples: - - input: | - # Bad: Rule file in wrong location - rules/my-rule.mdc - my-rule.mdc - .rules/my-rule.mdc - - # Good: Rule file in correct location - .cursor/rules/my-rule.mdc - output: "Correctly placed Cursor rule file" - -metadata: - priority: high - version: 1.0 - \ No newline at end of file diff --git a/packages/cli/template/extras/_cursor/rules/filemaker-api.mdc b/packages/cli/template/extras/_cursor/rules/filemaker-api.mdc deleted file mode 100644 index dd6d6716..00000000 --- a/packages/cli/template/extras/_cursor/rules/filemaker-api.mdc +++ /dev/null @@ -1,176 +0,0 @@ ---- -description: | - This rule provides guidance for working with the FileMaker Data API in this project. It should be included when: - 1. Working with database operations or data fetching - 2. Encountering database-related errors or type issues - 3. Making changes to FileMaker schemas or layouts - 4. Implementing new data access patterns - 5. Discussing alternative data storage solutions - 6. Working with server-side API routes or actions -globs: - - "src/**/*.ts" - - "src/**/*.tsx" - - "**/fmschema.config.mjs" - - "src/**/actions/*.ts" -alwaysApply: true ---- -# FileMaker Data API Integration - -This rule documents how the FileMaker Data API is integrated and used in the project. - - -name: filemaker_api -description: Documents FileMaker Data API integration patterns and conventions. FileMaker is the ONLY data source for this application - no SQL or other databases should be used. -filters: - - type: file_extension - pattern: "\\.(ts|tsx)$" - - type: directory - pattern: "src/server/" - - type: content - pattern: "(@proofkit/cli|ZodError|typegen)" - -data_source_policy: - exclusive_source: "FileMaker Data API" - prohibited: - - "SQL databases" - - "NoSQL databases" - - "Local storage for persistent data" - - "Direct file system storage" - reason: "All data operations must go through FileMaker to maintain data integrity and business logic" - -troubleshooting: - priority_order: - - "ALWAYS run `{package-manager} typegen` first for ANY data loading issues" - - "DO NOT check environment variables unless you have a specific error message pointing to them" - - "Check for FileMaker schema changes" - - "Verify type definitions match current schema" - - "Review Zod validation errors" - rationale: "Most data loading issues are resolved by running typegen. Environment variables are rarely the cause of data loading problems and should not be investigated unless specific error messages indicate an authentication or connection issue." - -conventions: - api_setup: - - Uses @proofkit/fmdapi package version ^5.0.0 - - Configuration in fmschema.config.mjs - - Environment variables in .env for connection details - - Type generation via `{package-manager} typegen` command - - data_access: - - ALL data operations MUST use FileMaker Data API - - Server-side only API calls via @proofkit/fmdapi - - Type-safe database operations - - Centralized error handling - - Connection pooling and session management - - No direct database connections outside FileMaker - - data_operations: - create: - - Use layout.create({ fieldData: {...} }) - - Validate input against Zod schemas - - Returns recordId of created record - - Handle duplicates via FileMaker business logic - read: - - Use layout.get({ recordId }) for single record by ID - - Use layout.find({ query, limit, offset, sort }) for multiple records - - Use layout.maybeFindFirst({ query }) for optional single record - - Support for complex queries and sorting - update: - - Use layout.update({ recordId, fieldData }) - - Follow FileMaker field naming conventions - - Respect FileMaker validation rules - delete: - - Use layout.delete({ recordId }) - - Respect FileMaker deletion rules - - Handle cascading deletes via FileMaker - query_options: - - Limit and offset for pagination - - Sort by multiple fields with ascend/descend - - Complex query criteria with operators (==, *, etc.) - - Optional type-safe responses with Zod validation - - security: - - Credentials stored in environment variables - - No direct client-side FM API access - - API routes validate authentication - - Data sanitization before queries - - All database access through FileMaker only - -type_generation: - process: - - "IMPORTANT: Running `{package-manager} typegen` solves almost all data loading problems" - - "Run `{package-manager} typegen` after any FileMaker schema changes" - - "Run `{package-manager} typegen` as first step when troubleshooting data issues" - - "Types are generated from FileMaker database schema" - - "Generated types are used in server actions and components" - - "Zod schemas validate runtime data against types" - - common_issues: - schema_changes: - symptoms: - - "No data appearing in tables" - - "ZodError during runtime" - - "Missing or renamed fields" - - "Type mismatches in responses" - - "Empty query results" - solution: "ALWAYS run `{package-manager} typegen && {package-manager} tsc` first" - important_note: "Do NOT check environment variables as a cause for data loading problems unless you have a specific known error that points to environment variables. Most data loading issues are resolved by running typegen." - - field_types: - symptoms: - - "Unexpected null values" - - "Type conversion errors" - - "Invalid date formats" - solution: "Update Zod schemas and type definitions" - - security_notes: - - "Never display, log, or commit environment variables" - - "Never check environment variable values directly" - - "Keep .env files out of version control" - - "When troubleshooting, only verify if variables exist, never their values" - -patterns: - - Server actions wrap FM API calls - - Type definitions generated from FM schema - - Error boundaries for FM API errors - - Rate limiting on API routes - - Caching strategies for frequent queries - -dependencies: - fmdapi: "@proofkit/fmdapi@^5.0.0" - proofkit: "@proofkit/cli@^1.0.0" - -keywords: - database: - - "FileMaker" - - "FMREST" - - "Database schema" - - "Field types" - - "Type generation" - - "Schema changes" - - "Exclusive data source" - - "No SQL" - - "FileMaker only" - - "Data API" - errors: - - "ZodError" - - "TypeError" - - "ValidationError" - - "Missing field" - - "Runtime error" - commands: - - "typegen" - - "tsc" - - "type checking" - - "schema update" - operations: - - "FM.create" - - "FM.find" - - "FM.get" - - "FM.update" - - "FM.delete" - - "FileMaker layout" - - "FileMaker query" - -metadata: - priority: high - version: 1.0 - \ No newline at end of file diff --git a/packages/cli/template/extras/_cursor/rules/troubleshooting-patterns.mdc b/packages/cli/template/extras/_cursor/rules/troubleshooting-patterns.mdc deleted file mode 100644 index 797fd3cc..00000000 --- a/packages/cli/template/extras/_cursor/rules/troubleshooting-patterns.mdc +++ /dev/null @@ -1,240 +0,0 @@ ---- -description: | -globs: -alwaysApply: false ---- -# Troubleshooting and Maintenance Patterns - -This rule documents common issues, error patterns, and their solutions in the project. - - -name: troubleshooting_patterns -description: Documents common runtime errors, type errors, and solutions. All data operations MUST use FileMaker Data API exclusively. -filters: - - type: file_extension - pattern: "\\.(ts|tsx)$" - - type: content - pattern: "(Error|error|ZodError|TypeError|ValidationError|@proofkit/fmdapi)" - -initial_debugging_steps: - priority: "ALWAYS run `{package-manager} typegen` first for any data-related issues" - steps: - - "Run `{package-manager} typegen` to ensure types match FileMaker schema" - - "Check if error persists after typegen" - - "If error persists, check console for exact error messages" - - "Look for patterns in the troubleshooting guide below" - common_console_errors: - zod_errors: - pattern: "ZodError: [path] invalid_type..." - likely_cause: "Field name mismatch or missing field" - example: "ZodError: nameFirst expected string, got undefined" - solution: "Run typegen first, then check field names in FileMaker schema" - type_errors: - pattern: "TypeError: Cannot read property 'X' of undefined" - likely_cause: "Accessing field before data is loaded or field name mismatch" - solution: "Run typegen first, then add null checks or loading states" - network_errors: - pattern: "Failed to fetch" or "Network error" - likely_cause: "FileMaker connection issues" - solution: "Run typegen first, then check FileMaker server status and credentials" - -data_source_validation: - requirement: "All data operations must use FileMaker Data API exclusively" - first_step_for_data_issues: "ALWAYS run `{package-manager} typegen` first" - common_mistakes: - - "Attempting to use SQL queries" - - "Adding direct database connections" - - "Using local storage for persistent data" - - "Implementing alternative data stores" - - "Skipping typegen after FileMaker schema changes" - - "Using incorrect field names from old schema" - correct_approach: - - "Run typegen first" - - "Use @proofkit/fmdapi for all data operations" - - "Follow FileMaker layout and field conventions" - - "Use layout.create, layout.find, layout.get, layout.update, layout.delete" - - "Use layout.maybeFindFirst for optional records" - -error_patterns: - field_name_mismatches: - symptoms: - - "ZodError: invalid_type at path [fieldName]" - - "Property 'X' does not exist on type 'Y'" - - "TypeScript errors about missing properties" - common_examples: - - "nameFirst vs firstName" - - "lastName vs nameLast" - - "postalCode vs postal_code" - - "phoneNumber vs phone" - cause: "Mismatch between component field names and FileMaker schema" - solution: - steps: - - "Run `{package-manager} typegen` to update types" - - "Look at generated types in src/config/schemas/filemaker/" - - "Update component field names to match schema" - - "Check console for exact field name in error" - files_to_check: - - "src/config/schemas/filemaker/*.ts" - - "Component files using the fields" - - zod_validation_errors: - symptoms: - - "Runtime ZodError: invalid_type" - - "Zod schema validation failed" - - "Property not found in schema" - - "Unexpected field in response" - cause: "FileMaker database schema changes not reflected in TypeScript types" - solution: - steps: - - "Run `{package-manager} typegen` to regenerate types from FileMaker schema" - - "Run `{package-manager} tsc` to identify type mismatches" - - "Check console for exact error message" - - "Update affected components and server actions" - commands: - - "{package-manager} typegen" - - "{package-manager} tsc" - files_to_check: - - "src/server/actions/*" - - "src/server/schema/*" - - "fmschema.config.mjs" - - filemaker_connection: - symptoms: - - "ETIMEDOUT connecting to FileMaker" - - "Invalid FileMaker credentials" - - "Session token expired" - - "Layout not found" - - "Field not found in layout" - - "Invalid find criteria" - - "No data appearing or queries returning empty" - cause: "FileMaker connection, authentication, or query issues" - solution: - steps: - - "Run `{package-manager} typegen` to ensure schema is up to date" - - "Check FileMaker Server status" - - "Validate credentials and permissions" - - "Note: As an AI, you cannot directly check environment variables - always ask the user to verify them if this is determined to be the issue" - - "Verify layout names and field access" - - "Check FileMaker query syntax" - files_to_check: - - "src/server/lib/fm.ts" - - "fmschema.config.mjs" - - data_access_errors: - symptoms: - - "Invalid operation on FileMaker record" - - "Record not found" - - "Insufficient permissions" - - "Invalid find request" - cause: "Incorrect FileMaker Data API usage or permissions" - solution: - steps: - - "Run `{package-manager} typegen` to ensure schema is up to date" - - "Verify FileMaker layout privileges" - - "Check record existence before operations" - - "Validate find criteria format" - - "Use proper FM API methods" - files_to_check: - - "src/server/actions/*" - - "src/server/lib/fm.ts" - - type_errors: - symptoms: - - "Type ... is not assignable to type ..." - - "Property ... does not exist on type ..." - - "Argument of type ... is not assignable" - cause: "Mismatch between FileMaker schema and TypeScript types" - solution: - steps: - - "Run `{package-manager} typegen` to regenerate types" - - "Run `{package-manager} tsc` to identify type mismatches" - - "Update type definitions if needed" - - "Check for null/undefined handling" - commands: - - "{package-manager} typegen && {package-manager} tsc" - - data_sync_issues: - symptoms: - - "Missing fields in table" - - "Unexpected null values" - - "Fields showing as blank" - - "Type mismatches between FM and frontend" - first_step: "ALWAYS run `{package-manager} typegen` first" - cause: "Mismatch between FileMaker schema and TypeScript types, or outdated type definitions" - solution: - steps: - - "Run `{package-manager} typegen` to regenerate types from FileMaker schema" - - "Check for any type errors in the console" - - "Verify field names match exactly between FM and generated types" - - "Update components if field names have changed" - commands: - - "{package-manager} typegen" - - "{package-manager} tsc" - files_to_check: - - "src/config/schemas/filemaker/*.ts" - - "fmschema.config.mjs" - -maintenance_tasks: - schema_sync: - description: "Keep FileMaker schema and TypeScript types in sync" - frequency: "After any FileMaker schema changes" - steps: - - "Run typegen to update types" - - "Run TypeScript compiler" - - "Update affected components" - impact: "Prevents runtime errors and type mismatches" - - type_checking: - description: "Regular type checking for early error detection" - frequency: "Before deployments and after schema changes" - commands: - - "{package-manager} tsc --noEmit" - impact: "Catches type errors before runtime" - -keywords: - errors: - - "ZodError" - - "TypeError" - - "ValidationError" - - "Schema mismatch" - - "Type mismatch" - - "Runtime error" - - "Database schema" - - "Type generation" - - "FileMaker fields" - - "Missing property" - - "Invalid type" - - "Layout not found" - - "Field not found" - - "Invalid find request" - solutions: - - "typegen" - - "tsc" - - "type checking" - - "schema update" - - "validation fix" - - "error handling" - - "FM API methods" - - "FileMaker layout" - operations: - - "layout.create" - - "layout.find" - - "layout.get" - - "layout.update" - - "layout.delete" - - "layout.maybeFindFirst" - - "recordId" - - "fieldData" - - "query parameters" - - "sort options" - data_source: - - "FileMaker only" - - "No SQL" - - "FM Data API" - - "Exclusive data source" - - "@proofkit/fmdapi" - -metadata: - priority: high - version: 1.0 - \ No newline at end of file diff --git a/packages/cli/template/extras/_cursor/rules/ui-components.mdc b/packages/cli/template/extras/_cursor/rules/ui-components.mdc deleted file mode 100644 index 78ec63ad..00000000 --- a/packages/cli/template/extras/_cursor/rules/ui-components.mdc +++ /dev/null @@ -1,57 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- -# UI Components and Styling - -This rule documents the UI component library and styling conventions used in the project. - - -name: ui_components -description: Documents UI component library usage and styling conventions -filters: - - type: file_extension - pattern: "\\.(ts|tsx)$" - - type: directory - pattern: "src/components/" - - type: content - pattern: "@mantine/" - -conventions: - component_library: - - Mantine v7 as primary UI framework - - Tabler icons for iconography - - Mantine React Table for data grids - - Custom components extend Mantine base - - styling: - - PostCSS for processing - - Mantine theme customization - - CSS modules for component styles - - CSS variables for theming - - components: - - Atomic design principles - - Consistent prop interfaces - - Accessibility first - - Responsive design patterns - - forms: - - React Hook Form for form state - - Zod for validation schemas - - Mantine form components - - Custom form layouts - -dependencies: - mantine_core: "^7.17.0" - mantine_hooks: "^7.17.0" - mantine_dates: "^7.17.0" - mantine_notifications: "^7.17.0" - react_hook_form: "^7.54.2" - zod: "^3.24.2" - -metadata: - priority: high - version: 1.0 - \ No newline at end of file diff --git a/packages/cli/template/extras/config/drizzle-config-mysql.ts b/packages/cli/template/extras/config/drizzle-config-mysql.ts deleted file mode 100644 index 63ba29ab..00000000 --- a/packages/cli/template/extras/config/drizzle-config-mysql.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Config } from "drizzle-kit"; - -import { env } from "~/env"; - -export default { - schema: "./src/server/db/schema.ts", - dialect: "mysql", - dbCredentials: { - url: env.DATABASE_URL, - }, - tablesFilter: ["project1_*"], -} satisfies Config; diff --git a/packages/cli/template/extras/config/drizzle-config-postgres.ts b/packages/cli/template/extras/config/drizzle-config-postgres.ts deleted file mode 100644 index 063cc964..00000000 --- a/packages/cli/template/extras/config/drizzle-config-postgres.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Config } from "drizzle-kit"; - -import { env } from "~/env"; - -export default { - schema: "./src/server/db/schema.ts", - dialect: "postgresql", - dbCredentials: { - url: env.DATABASE_URL, - }, - tablesFilter: ["project1_*"], -} satisfies Config; diff --git a/packages/cli/template/extras/config/drizzle-config-sqlite.ts b/packages/cli/template/extras/config/drizzle-config-sqlite.ts deleted file mode 100644 index b55a91e0..00000000 --- a/packages/cli/template/extras/config/drizzle-config-sqlite.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Config } from "drizzle-kit"; - -import { env } from "~/env"; - -export default { - schema: "./src/server/db/schema.ts", - dialect: "sqlite", - dbCredentials: { - url: env.DATABASE_URL, - }, - tablesFilter: ["project1_*"], -} satisfies Config; diff --git a/packages/cli/template/extras/config/fmschema.config.mjs b/packages/cli/template/extras/config/fmschema.config.mjs deleted file mode 100644 index 4b940151..00000000 --- a/packages/cli/template/extras/config/fmschema.config.mjs +++ /dev/null @@ -1,9 +0,0 @@ -/** @type {import("@proofkit/fmdapi/dist/utils/typegen/types.d.ts").GenerateSchemaOptions} */ -export const config = { - clientSuffix: "Layout", - schemas: [ - // add your layouts and name schemas here - ], - clearOldFiles: true, - path: "./src/config/schemas/filemaker", -}; diff --git a/packages/cli/template/extras/config/get-query-client.ts b/packages/cli/template/extras/config/get-query-client.ts deleted file mode 100644 index 44598cba..00000000 --- a/packages/cli/template/extras/config/get-query-client.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { QueryClient } from "@tanstack/react-query"; -import { cache } from "react"; - -// cache() is scoped per request, so we don't leak data between requests -const getQueryClient = cache(() => new QueryClient()); -export default getQueryClient; diff --git a/packages/cli/template/extras/config/postcss.config.cjs b/packages/cli/template/extras/config/postcss.config.cjs deleted file mode 100644 index 51ec8fa6..00000000 --- a/packages/cli/template/extras/config/postcss.config.cjs +++ /dev/null @@ -1,7 +0,0 @@ -const config = { - plugins: { - tailwindcss: {}, - }, -}; - -module.exports = config; diff --git a/packages/cli/template/extras/config/query-provider-vite.tsx b/packages/cli/template/extras/config/query-provider-vite.tsx deleted file mode 100644 index 7b1d7e2b..00000000 --- a/packages/cli/template/extras/config/query-provider-vite.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; - -const queryClient = new QueryClient(); - -export default function QueryProvider({ - children, -}: { - children: React.ReactNode; -}) { - return ( - - {children} - - - ); -} diff --git a/packages/cli/template/extras/config/query-provider.tsx b/packages/cli/template/extras/config/query-provider.tsx deleted file mode 100644 index 2a928906..00000000 --- a/packages/cli/template/extras/config/query-provider.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import { QueryClientProvider } from "@tanstack/react-query"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; - -import getQueryClient from "./get-query-client"; - -export default function QueryProvider({ - children, -}: { - children: React.ReactNode; -}) { - const queryClient = getQueryClient(); - - return ( - - {children} - - - ); -} diff --git a/packages/cli/template/extras/emailProviders/none/email.tsx b/packages/cli/template/extras/emailProviders/none/email.tsx deleted file mode 100644 index 7cf42f10..00000000 --- a/packages/cli/template/extras/emailProviders/none/email.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { render } from "@react-email/render"; -import { AuthCodeEmail } from "@/emails/auth-code"; - -export async function sendEmail({ - to, - code, - type, -}: { - to: string; - code: string; - type: "verification" | "password-reset"; -}) { - // this is the HTML body of the email to be send - const body = await render( - , - ); - const subject = - type === "verification" ? "Verify Your Email" : "Reset Your Password"; - - // TODO: Customize this function to actually send the email to your users - // Learn more: https://proofkit.proof.sh/auth/fm-addon - console.warn("TODO: Customize this function to actually send to your users"); - console.log(`To ${to}: Your ${type} code is ${code}`); -} diff --git a/packages/cli/template/extras/emailProviders/plunk/email.tsx b/packages/cli/template/extras/emailProviders/plunk/email.tsx deleted file mode 100644 index d1e8acc7..00000000 --- a/packages/cli/template/extras/emailProviders/plunk/email.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { render } from "@react-email/render"; -import { AuthCodeEmail } from "@/emails/auth-code"; - -import { plunk } from "../services/plunk"; - -export async function sendEmail({ - to, - code, - type, -}: { - to: string; - code: string; - type: "verification" | "password-reset"; -}) { - // this is the HTML body of the email to be send - const body = await render( - , - ); - const subject = - type === "verification" ? "Verify Your Email" : "Reset Your Password"; - - await plunk.emails.send({ - to, - subject, - body, - }); -} diff --git a/packages/cli/template/extras/emailProviders/plunk/service.ts b/packages/cli/template/extras/emailProviders/plunk/service.ts deleted file mode 100644 index 080ae050..00000000 --- a/packages/cli/template/extras/emailProviders/plunk/service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import Plunk from "@plunk/node"; -import { env } from "@/config/env"; - -export const plunk = new Plunk(env.PLUNK_API_KEY); diff --git a/packages/cli/template/extras/emailProviders/resend/email.tsx b/packages/cli/template/extras/emailProviders/resend/email.tsx deleted file mode 100644 index 0ff4601a..00000000 --- a/packages/cli/template/extras/emailProviders/resend/email.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { AuthCodeEmail } from "@/emails/auth-code"; - -import { resend } from "../services/resend"; - -export async function sendEmail({ - to, - code, - type, -}: { - to: string; - code: string; - type: "verification" | "password-reset"; -}) { - const subject = - type === "verification" ? "Verify Your Email" : "Reset Your Password"; - - await resend.emails.send({ - // TODO: Change this to our own email after verifying your domain with Resend - from: "ProofKit ", - to, - subject, - react: , - }); -} diff --git a/packages/cli/template/extras/emailProviders/resend/service.ts b/packages/cli/template/extras/emailProviders/resend/service.ts deleted file mode 100644 index 6adeac4f..00000000 --- a/packages/cli/template/extras/emailProviders/resend/service.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Resend } from "resend"; -import { env } from "@/config/env"; - -export const resend = new Resend(env.RESEND_API_KEY); diff --git a/packages/cli/template/extras/emailTemplates/auth-code.tsx b/packages/cli/template/extras/emailTemplates/auth-code.tsx deleted file mode 100644 index 1e7191f2..00000000 --- a/packages/cli/template/extras/emailTemplates/auth-code.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { - Body, - Container, - Head, - Heading, - Html, - Img, - Section, - Text, -} from "@react-email/components"; - -interface AuthCodeEmailProps { - validationCode: string; - type: "verification" | "password-reset"; -} - -export const AuthCodeEmail = ({ validationCode, type }: AuthCodeEmailProps) => ( - - - - - ProofKit - - {type === "verification" - ? "Verify Your Email" - : "Reset Your Password"} - - - Enter the following code to{" "} - {type === "verification" - ? "verify your email" - : "reset your password"} - -
- {validationCode} -
- - If you did not request this code, you can ignore this email. - -
- - -); - -AuthCodeEmail.PreviewProps = { - validationCode: "D7CU4GOV", - type: "verification", -} as AuthCodeEmailProps; - -export default AuthCodeEmail; - -const main = { - backgroundColor: "#ffffff", - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", -}; - -const container = { - backgroundColor: "#ffffff", - border: "1px solid #eee", - borderRadius: "5px", - boxShadow: "0 5px 10px rgba(20,50,70,.2)", - marginTop: "20px", - maxWidth: "360px", - margin: "0 auto", - padding: "68px 0 130px", -}; - -const logo: React.CSSProperties = { - margin: "0 auto", -}; - -const tertiary = { - color: "#0a85ea", - fontSize: "11px", - fontWeight: 700, - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", - height: "16px", - letterSpacing: "0", - lineHeight: "16px", - margin: "16px 8px 8px 8px", - textTransform: "uppercase" as const, - textAlign: "center" as const, -}; - -const secondary = { - color: "#000", - display: "inline-block", - fontFamily: "HelveticaNeue-Medium,Helvetica,Arial,sans-serif", - fontSize: "20px", - fontWeight: 500, - lineHeight: "24px", - marginBottom: "0", - marginTop: "0", - textAlign: "center" as const, - padding: "0 40px", -}; - -const codeContainer = { - background: "rgba(0,0,0,.05)", - borderRadius: "4px", - margin: "16px auto 14px", - verticalAlign: "middle", - width: "280px", -}; - -const code = { - color: "#000", - display: "inline-block", - fontFamily: "HelveticaNeue-Bold", - fontSize: "32px", - fontWeight: 700, - letterSpacing: "6px", - lineHeight: "40px", - paddingBottom: "8px", - paddingTop: "8px", - margin: "0 auto", - width: "100%", - textAlign: "center" as const, -}; - -const paragraph = { - color: "#444", - fontSize: "15px", - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", - letterSpacing: "0", - lineHeight: "23px", - padding: "0 40px", - margin: "0", - textAlign: "center" as const, -}; - -const link = { - color: "#444", - textDecoration: "underline", -}; - -const footer = { - color: "#000", - fontSize: "12px", - fontWeight: 800, - letterSpacing: "0", - lineHeight: "23px", - margin: "0", - marginTop: "20px", - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", - textAlign: "center" as const, - textTransform: "uppercase" as const, -}; diff --git a/packages/cli/template/extras/emailTemplates/generic.tsx b/packages/cli/template/extras/emailTemplates/generic.tsx deleted file mode 100644 index 33e4ef49..00000000 --- a/packages/cli/template/extras/emailTemplates/generic.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { - Body, - Button, - Container, - Head, - Heading, - Hr, - Html, - Img, - Section, - Text, -} from "@react-email/components"; - -export interface GenericEmailProps { - title?: string; - description?: string; - ctaText?: string; - ctaHref?: string; - footer?: string; -} - -export const GenericEmail = ({ - title, - description, - ctaText, - ctaHref, - footer, -}: GenericEmailProps) => ( - - - - - ProofKit - - {title ? {title} : null} - - {description ? ( - {description} - ) : null} - - {ctaText && ctaHref ? ( -
- -
- ) : null} - - {(title || description || (ctaText && ctaHref)) && ( -
- )} - - {footer ? {footer} : null} -
- - -); - -GenericEmail.PreviewProps = { - title: "Welcome to ProofKit", - description: - "Thanks for trying ProofKit. This is a sample email template you can customize.", - ctaText: "Get Started", - ctaHref: "https://proofkit.proof.sh", - footer: "You received this email because you signed up for updates.", -} as GenericEmailProps; - -export default GenericEmail; - -const styles = { - main: { - backgroundColor: "#ffffff", - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", - }, - container: { - backgroundColor: "#ffffff", - border: "1px solid #eee", - borderRadius: "5px", - boxShadow: "0 5px 10px rgba(20,50,70,.2)", - marginTop: "20px", - maxWidth: "520px", - margin: "0 auto", - padding: "48px 32px 36px", - } as React.CSSProperties, - logo: { - margin: "0 auto 12px", - display: "block", - } as React.CSSProperties, - title: { - color: "#111827", - fontSize: "22px", - fontWeight: 600, - lineHeight: "28px", - margin: "8px 0 4px", - textAlign: "center" as const, - }, - description: { - color: "#374151", - fontSize: "15px", - lineHeight: "22px", - margin: "8px 0 0", - textAlign: "center" as const, - }, - ctaSection: { - textAlign: "center" as const, - marginTop: "20px", - }, - ctaButton: { - backgroundColor: "#0a85ea", - color: "#fff", - fontSize: "14px", - fontWeight: 600, - lineHeight: "20px", - textDecoration: "none", - display: "inline-block", - padding: "10px 16px", - borderRadius: "6px", - } as React.CSSProperties, - hr: { - borderColor: "#e5e7eb", - margin: "24px 0 12px", - }, - footer: { - color: "#6b7280", - fontSize: "12px", - lineHeight: "18px", - textAlign: "center" as const, - }, -}; diff --git a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts b/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts deleted file mode 100644 index 586f9c74..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/actions.ts +++ /dev/null @@ -1,97 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -import { - createEmailVerificationRequest, - sendVerificationEmail, - setEmailVerificationRequestCookie, -} from "@/server/auth/utils/email-verification"; -import { - verifyPasswordHash, - verifyPasswordStrength, -} from "@/server/auth/utils/password"; -import { - createSession, - generateSessionToken, - getCurrentSession, - invalidateUserSessions, - setSessionTokenCookie, -} from "@/server/auth/utils/session"; -import { - checkEmailAvailability, - updateUserPassword, - validateLogin, -} from "@/server/auth/utils/user"; -import { actionClient } from "@/server/safe-action"; - -import { updateEmailSchema, updatePasswordSchema } from "./schema"; - -export const updateEmailAction = actionClient - .schema(updateEmailSchema) - .action(async ({ parsedInput }) => { - const { session, user } = await getCurrentSession(); - if (session === null) { - return { - message: "Not authenticated", - }; - } - - const { email } = parsedInput; - - const emailAvailable = await checkEmailAvailability(email); - if (!emailAvailable) { - return { - error: "This email is already used", - }; - } - - const verificationRequest = await createEmailVerificationRequest( - user.id, - email, - ); - await sendVerificationEmail( - verificationRequest.email, - verificationRequest.code, - ); - await setEmailVerificationRequestCookie(verificationRequest); - return redirect("/auth/verify-email"); - }); - -export const updatePasswordAction = actionClient - .schema(updatePasswordSchema) - .action(async ({ parsedInput }) => { - const { confirmNewPassword, currentPassword, newPassword } = parsedInput; - - const { session, user } = await getCurrentSession(); - if (session === null) { - return { - error: "Not authenticated", - }; - } - - const strongPassword = await verifyPasswordStrength(newPassword); - if (!strongPassword) { - return { - error: "Weak password", - }; - } - - const validPassword = Boolean( - await validateLogin(user.email, currentPassword), - ); - if (!validPassword) { - return { - error: "Incorrect password", - }; - } - - await invalidateUserSessions(user.id); - await updateUserPassword(user.id, newPassword); - - const sessionToken = generateSessionToken(); - const newSession = await createSession(sessionToken, user.id); - await setSessionTokenCookie(sessionToken, newSession.expiresAt); - return { - message: "Password updated", - }; - }); diff --git a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx b/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx deleted file mode 100644 index f6dfe126..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Anchor, Container, Paper, Stack, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; -import { getCurrentSession } from "@/server/auth/utils/session"; - -import UpdateEmailForm from "./profile-form"; -import UpdatePasswordForm from "./reset-password-form"; - -// import EmailVerificationForm from "./email-verification-form"; - -export default async function Page() { - const { user } = await getCurrentSession(); - - if (user === null) { - return redirect("/auth/login"); - } - - return ( - - Profile Details - - - - - - - - - ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx b/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx deleted file mode 100644 index 9fe14bf7..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/profile-form.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { - Anchor, - Button, - Group, - Paper, - PasswordInput, - Stack, - Text, - TextInput, -} from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; - -import { updateEmailAction } from "./actions"; -import { updateEmailSchema } from "./schema"; - -export default function UpdateEmailForm({ - currentEmail, -}: { - currentEmail: string; -}) { - const { form, handleSubmitWithAction, action } = useHookFormAction( - updateEmailAction, - zodResolver(updateEmailSchema), - { formProps: { defaultValues: { email: currentEmail } } }, - ); - - return ( -
- - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - {form.formState.isDirty && ( - - - - )} - -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx b/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx deleted file mode 100644 index 896a4d25..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/reset-password-form.tsx +++ /dev/null @@ -1,112 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { - Anchor, - Button, - Group, - Paper, - PasswordInput, - Stack, - Text, - TextInput, -} from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; -import { useState } from "react"; -import { showSuccessNotification } from "@/utils/notification-helpers"; - -import { updatePasswordAction } from "./actions"; -import { updatePasswordSchema } from "./schema"; - -export default function UpdatePasswordForm() { - const [showForm, setShowForm] = useState(false); - const { form, handleSubmitWithAction, action } = useHookFormAction( - updatePasswordAction, - zodResolver(updatePasswordSchema), - { - formProps: { defaultValues: {} }, - actionProps: { - onSuccess: ({ data }) => { - if (data?.message) { - showSuccessNotification(data.message); - setShowForm(false); - } - }, - }, - }, - ); - - if (!showForm) { - return ( - - - - - ); - } - - return ( -
- - - - - - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - - - -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts b/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts deleted file mode 100644 index 3984756a..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/(main)/auth/profile/schema.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { z } from "zod/v4"; - -export const updateEmailSchema = z.object({ - email: z.string().email(), -}); - -export const updatePasswordSchema = z - .object({ - currentPassword: z.string(), - newPassword: z - .string() - .min(8, { message: "Password must be at least 8 characters long" }) - .max(255, { message: "Password is too long" }), - confirmNewPassword: z.string(), - }) - .refine((data) => data.newPassword === data.confirmNewPassword, { - path: ["confirmNewPassword"], - message: "Passwords do not match", - }); diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts deleted file mode 100644 index bdd2d779..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/actions.ts +++ /dev/null @@ -1,39 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -import { - createPasswordResetSession, - invalidateUserPasswordResetSessions, - sendPasswordResetEmail, - setPasswordResetSessionTokenCookie, -} from "@/server/auth/utils/password-reset"; -import { generateSessionToken } from "@/server/auth/utils/session"; -import { getUserFromEmail } from "@/server/auth/utils/user"; -import { actionClient } from "@/server/safe-action"; - -import { forgotPasswordSchema } from "./schema"; - -export const forgotPasswordAction = actionClient - .schema(forgotPasswordSchema) - .action(async ({ parsedInput }) => { - const { email } = parsedInput; - - const user = await getUserFromEmail(email); - if (user === null) { - return { - error: "Account does not exist", - }; - } - - await invalidateUserPasswordResetSessions(user.id); - const sessionToken = generateSessionToken(); - const session = await createPasswordResetSession( - sessionToken, - user.id, - user.email, - ); - - await sendPasswordResetEmail(session.email, session.code); - await setPasswordResetSessionTokenCookie(sessionToken, session.expires_at); - return redirect("/auth/reset-password/verify-email"); - }); diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx deleted file mode 100644 index 1b74f9ba..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/forgot-form.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { Button, Paper, Stack, Text, TextInput } from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; - -import { forgotPasswordAction } from "./actions"; -import { forgotPasswordSchema } from "./schema"; - -export default function ForgotForm() { - const { form, handleSubmitWithAction, action } = useHookFormAction( - forgotPasswordAction, - zodResolver(forgotPasswordSchema), - {}, - ); - - return ( -
- - - - - {action.result.data?.error && ( - {action.result.data.error} - )} - - - - -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx deleted file mode 100644 index a3c2bc7a..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/page.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Anchor, Container, Text, Title } from "@mantine/core"; - -import ForgotForm from "./forgot-form"; - -export default async function Page() { - return ( - - Forgot Password - - Enter your email for a link to reset your password. - - - - - - - Back to login - - - - ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts deleted file mode 100644 index 618832e7..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/forgot-password/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from "zod/v4"; - -export const forgotPasswordSchema = z.object({ - email: z.string().email(), -}); diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/login/actions.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/login/actions.ts deleted file mode 100644 index 06e4a939..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/login/actions.ts +++ /dev/null @@ -1,35 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -import { getRedirectCookie } from "@/server/auth/utils/redirect"; -import { - createSession, - generateSessionToken, - setSessionTokenCookie, -} from "@/server/auth/utils/session"; -import { validateLogin } from "@/server/auth/utils/user"; -import { actionClient } from "@/server/safe-action"; - -import { loginSchema } from "./schema"; - -export const loginAction = actionClient - .schema(loginSchema) - .action(async ({ parsedInput }) => { - const { email, password } = parsedInput; - const user = await validateLogin(email, password); - - if (user === null) { - return { error: "Invalid email or password" }; - } - - const sessionToken = generateSessionToken(); - const session = await createSession(sessionToken, user.id); - setSessionTokenCookie(sessionToken, session.expiresAt); - - if (!user.emailVerified) { - return redirect("/auth/verify-email"); - } - - const redirectTo = await getRedirectCookie(); - return redirect(redirectTo); - }); diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/login/login-form.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/login/login-form.tsx deleted file mode 100644 index 41e3c938..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/login/login-form.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { - Anchor, - Button, - Group, - Paper, - PasswordInput, - Stack, - Text, - TextInput, -} from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; - -import { loginAction } from "./actions"; -import { loginSchema } from "./schema"; - -export default function LoginForm() { - const { form, handleSubmitWithAction, action } = useHookFormAction( - loginAction, - zodResolver(loginSchema), - {}, - ); - - return ( -
- - - - - - - - Forgot password? - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/login/page.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/login/page.tsx deleted file mode 100644 index 0f66ba7a..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/login/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Anchor, Container, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; -import { getCurrentSession } from "@/server/auth/utils/session"; - -import LoginForm from "./login-form"; - -export default async function Page() { - const { session } = await getCurrentSession(); - - if (session !== null) { - return redirect("/"); - } - - return ( - - Welcome back! - - Do not have an account yet?{" "} - - Create account - - - - - - ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/login/schema.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/login/schema.ts deleted file mode 100644 index 95feeb80..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/login/schema.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { z } from "zod/v4"; - -export const loginSchema = z.object({ - email: z.string().email(), - password: z.string(), -}); diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts deleted file mode 100644 index 7e7a46d2..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/actions.ts +++ /dev/null @@ -1,53 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -import { verifyPasswordStrength } from "@/server/auth/utils/password"; -import { - deletePasswordResetSessionTokenCookie, - invalidateUserPasswordResetSessions, - validatePasswordResetSessionRequest, -} from "@/server/auth/utils/password-reset"; -import { - createSession, - generateSessionToken, - invalidateUserSessions, - setSessionTokenCookie, -} from "@/server/auth/utils/session"; -import { updateUserPassword } from "@/server/auth/utils/user"; -import { actionClient } from "@/server/safe-action"; - -import { resetPasswordSchema } from "./schema"; - -export const resetPasswordAction = actionClient - .schema(resetPasswordSchema) - .action(async ({ parsedInput }) => { - const { password } = parsedInput; - const { session: passwordResetSession, user } = - await validatePasswordResetSessionRequest(); - if (passwordResetSession === null) { - return { - error: "Not authenticated", - }; - } - if (!passwordResetSession.email_verified) { - return { - error: "Forbidden", - }; - } - - const strongPassword = await verifyPasswordStrength(password); - if (!strongPassword) { - return { - error: "Weak password", - }; - } - await invalidateUserPasswordResetSessions(passwordResetSession.id_user); - await invalidateUserSessions(passwordResetSession.id_user); - await updateUserPassword(passwordResetSession.id_user, password); - - const sessionToken = generateSessionToken(); - const session = await createSession(sessionToken, user.id); - await setSessionTokenCookie(sessionToken, session.expiresAt); - await deletePasswordResetSessionTokenCookie(); - return redirect("/"); - }); diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx deleted file mode 100644 index 6c25efe5..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Alert, Anchor, Container, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; -import { env } from "@/config/env"; -import { validatePasswordResetSessionRequest } from "@/server/auth/utils/password-reset"; - -import ResetPasswordForm from "./reset-password-form"; - -export default async function Page() { - const { session, user } = await validatePasswordResetSessionRequest(); - if (session === null) { - return redirect("/auth/forgot-password"); - } - if (!session.email_verified) { - return redirect("/auth/reset-password/verify-email"); - } - - return ( - - Reset Password - - Enter your new password. - - - - - - - Back to login - - - - ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx deleted file mode 100644 index 82d70c6c..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/reset-password-form.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { - Button, - Paper, - PasswordInput, - Stack, - Text, - TextInput, -} from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; - -import { resetPasswordAction } from "./actions"; -import { resetPasswordSchema } from "./schema"; - -export default function ForgotForm() { - const { form, handleSubmitWithAction, action } = useHookFormAction( - resetPasswordAction, - zodResolver(resetPasswordSchema), - {}, - ); - - return ( -
- - - - - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - - -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts deleted file mode 100644 index 3973a81f..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/schema.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z } from "zod/v4"; - -export const resetPasswordSchema = z - .object({ - password: z - .string() - .min(8, { message: "Your password should be at least 8 characters" }) - .max(255, { message: "Password is too long" }), - confirmPassword: z.string(), - }) - .refine((data) => data.password === data.confirmPassword, { - path: ["confirmPassword"], - message: "Passwords do not match", - }); diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts deleted file mode 100644 index 26ca4924..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/actions.ts +++ /dev/null @@ -1,46 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -import { - setPasswordResetSessionAsEmailVerified, - validatePasswordResetSessionRequest, -} from "@/server/auth/utils/password-reset"; -import { setUserAsEmailVerifiedIfEmailMatches } from "@/server/auth/utils/user"; -import { actionClient } from "@/server/safe-action"; - -import { verifyEmailSchema } from "./schema"; - -export const verifyEmailAction = actionClient - .schema(verifyEmailSchema) - .action(async ({ parsedInput }) => { - const { session } = await validatePasswordResetSessionRequest(); - if (session === null) { - return { - error: "Not authenticated", - }; - } - if (session.email_verified) { - return { - error: "Forbidden", - }; - } - - const { code } = parsedInput; - - if (code !== session.code) { - return { - error: "Incorrect code", - }; - } - await setPasswordResetSessionAsEmailVerified(session.id); - const emailMatches = await setUserAsEmailVerifiedIfEmailMatches( - session.id_user, - session.email, - ); - if (!emailMatches) { - return { - error: "Please restart the process", - }; - } - return redirect("/auth/reset-password"); - }); diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx deleted file mode 100644 index af23cee9..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Alert, Anchor, Container, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; -import { env } from "@/config/env"; -import { validatePasswordResetSessionRequest } from "@/server/auth/utils/password-reset"; - -import VerifyEmailForm from "./verify-email-form"; - -export default async function Page() { - const { session } = await validatePasswordResetSessionRequest(); - if (session === null) { - return redirect("/auth/forgot-password"); - } - if (session.email_verified) { - return redirect("/auth/reset-password"); - } - - return ( - - Verify Email - - Enter the code sent to your email. - - - - - - - Back to login - - - - ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts deleted file mode 100644 index efd19a95..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from "zod/v4"; - -export const verifyEmailSchema = z.object({ - code: z.string().length(8), -}); diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx deleted file mode 100644 index 07516c86..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/reset-password/verify-email/verify-email-form.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { Button, Paper, PinInput, Stack, Text } from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; - -import { verifyEmailAction } from "./actions"; -import { verifyEmailSchema } from "./schema"; - -export default function VerifyEmailForm() { - const { form, handleSubmitWithAction, action } = useHookFormAction( - verifyEmailAction, - zodResolver(verifyEmailSchema), - {}, - ); - - return ( -
- - - { - form.setValue("code", value); - if (value.length === 8) { - handleSubmitWithAction(); - } - }} - error={!!form.formState.errors.code?.message} - autoFocus - /> - {form.formState.errors.code?.message && ( - {form.formState.errors.code.message} - )} - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/signup/actions.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/signup/actions.ts deleted file mode 100644 index 40d77b3d..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/signup/actions.ts +++ /dev/null @@ -1,50 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -import { - createEmailVerificationRequest, - sendVerificationEmail, - setEmailVerificationRequestCookie, -} from "@/server/auth/utils/email-verification"; -import { verifyPasswordStrength } from "@/server/auth/utils/password"; -import { - createSession, - generateSessionToken, - setSessionTokenCookie, -} from "@/server/auth/utils/session"; -import { checkEmailAvailability, createUser } from "@/server/auth/utils/user"; -import { actionClient } from "@/server/safe-action"; - -import { signupSchema } from "./schema"; - -export const signupAction = actionClient - .schema(signupSchema) - .action(async ({ parsedInput }) => { - const { email, password } = parsedInput; - const emailAvailable = await checkEmailAvailability(email); - if (!emailAvailable) { - return { error: "Email already in use" }; - } - - const passwordStrong = await verifyPasswordStrength(password); - if (!passwordStrong) { - return { error: "Password is too weak" }; - } - - const user = await createUser(email, password); - const emailVerificationRequest = await createEmailVerificationRequest( - user.id, - user.email, - ); - await sendVerificationEmail( - emailVerificationRequest.email, - emailVerificationRequest.code, - ); - await setEmailVerificationRequestCookie(emailVerificationRequest); - - const sessionToken = generateSessionToken(); - const session = await createSession(sessionToken, user.id); - setSessionTokenCookie(sessionToken, session.expiresAt); - - return redirect("/auth/verify-email"); - }); diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/signup/page.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/signup/page.tsx deleted file mode 100644 index b9614dc7..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/signup/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Anchor, Container, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; -import { getCurrentSession } from "@/server/auth/utils/session"; - -import SignupForm from "./signup-form"; - -export default async function Page() { - const { session } = await getCurrentSession(); - - if (session !== null) { - return redirect("/"); - } - - return ( - - Create account - - Already have an account?{" "} - - Sign in - - - - - - ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/signup/schema.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/signup/schema.ts deleted file mode 100644 index 61ac2e86..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/signup/schema.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { z } from "zod/v4"; - -export const signupSchema = z - .object({ - email: z.string().email(), - password: z.string().min(8), - confirmPassword: z.string(), - }) - .refine((data) => data.password === data.confirmPassword, { - path: ["confirmPassword"], - message: "Passwords do not match", - }); diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx deleted file mode 100644 index 6b2868a4..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/signup/signup-form.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { - Anchor, - Button, - Group, - Paper, - PasswordInput, - Stack, - Text, - TextInput, -} from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; - -import { signupAction } from "./actions"; -import { signupSchema } from "./schema"; - -export default function SignupForm() { - const { form, handleSubmitWithAction, action } = useHookFormAction( - signupAction, - zodResolver(signupSchema), - {}, - ); - - return ( -
- - - - - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts deleted file mode 100644 index 3c79df22..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/actions.ts +++ /dev/null @@ -1,109 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -import { - createEmailVerificationRequest, - deleteEmailVerificationRequestCookie, - deleteUserEmailVerificationRequest, - getUserEmailVerificationRequestFromRequest, - sendVerificationEmail, - setEmailVerificationRequestCookie, -} from "@/server/auth/utils/email-verification"; -import { invalidateUserPasswordResetSessions } from "@/server/auth/utils/password-reset"; -import { getRedirectCookie } from "@/server/auth/utils/redirect"; -import { getCurrentSession } from "@/server/auth/utils/session"; -import { updateUserEmailAndSetEmailAsVerified } from "@/server/auth/utils/user"; -import { actionClient } from "@/server/safe-action"; - -import { emailVerificationSchema } from "./schema"; - -export const verifyEmailAction = actionClient - .schema(emailVerificationSchema) - .action(async ({ parsedInput, ctx }) => { - const { session, user } = await getCurrentSession(); - if (session === null) { - return { - error: "Not authenticated", - }; - } - - let verificationRequest = - await getUserEmailVerificationRequestFromRequest(); - if (verificationRequest === null) { - return { - error: "Not authenticated", - }; - } - const { code } = parsedInput; - if (verificationRequest.expires_at === null) { - return { - error: "Verification code expired", - }; - } - - if (Date.now() >= verificationRequest.expires_at * 1000) { - verificationRequest = await createEmailVerificationRequest( - verificationRequest.id_user, - verificationRequest.email, - ); - await sendVerificationEmail( - verificationRequest.email, - verificationRequest.code, - ); - return { - error: - "The verification code was expired. We sent another code to your inbox.", - }; - } - if (verificationRequest.code !== code) { - return { - error: "Incorrect code.", - }; - } - await deleteUserEmailVerificationRequest(user.id); - await invalidateUserPasswordResetSessions(user.id); - await updateUserEmailAndSetEmailAsVerified( - user.id, - verificationRequest.email, - ); - await deleteEmailVerificationRequestCookie(); - - const redirectTo = await getRedirectCookie(); - return redirect(redirectTo); - }); - -export const resendEmailVerificationAction = actionClient.action(async () => { - const { session, user } = await getCurrentSession(); - if (session === null) { - return { - error: "Not authenticated", - }; - } - - let verificationRequest = await getUserEmailVerificationRequestFromRequest(); - if (verificationRequest === null) { - if (user.emailVerified) { - return { - error: "Forbidden", - }; - } - - verificationRequest = await createEmailVerificationRequest( - user.id, - user.email, - ); - } else { - verificationRequest = await createEmailVerificationRequest( - user.id, - verificationRequest.email, - ); - } - await sendVerificationEmail( - verificationRequest.email, - verificationRequest.code, - ); - await setEmailVerificationRequestCookie(verificationRequest); - return { - message: "A new code was sent to your inbox.", - }; -}); diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx deleted file mode 100644 index f6cded7e..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/email-verification-form.tsx +++ /dev/null @@ -1,46 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { Button, Paper, PinInput, Stack, Text } from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; - -import { verifyEmailAction } from "./actions"; -import { emailVerificationSchema } from "./schema"; - -export default function LoginForm() { - const { form, handleSubmitWithAction, action } = useHookFormAction( - verifyEmailAction, - zodResolver(emailVerificationSchema), - {}, - ); - - return ( -
- - - { - form.setValue("code", value); - if (value.length === 8) { - handleSubmitWithAction(); - } - }} - /> - - {action.result.data?.error ? ( - {action.result.data.error} - ) : action.hasErrored ? ( - An error occured - ) : null} - - - -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx deleted file mode 100644 index 13e831ab..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Anchor, Container, Text, Title } from "@mantine/core"; -import { redirect } from "next/navigation"; -import { getUserEmailVerificationRequestFromRequest } from "@/server/auth/utils/email-verification"; -import { getRedirectCookie } from "@/server/auth/utils/redirect"; -import { getCurrentSession } from "@/server/auth/utils/session"; - -import EmailVerificationForm from "./email-verification-form"; -import ResendButton from "./resend-button"; - -export default async function Page() { - const { user } = await getCurrentSession(); - - if (user === null) { - return redirect("/auth/login"); - } - - // TODO: Ideally we'd sent a new verification email automatically if the previous one is expired, - // but we can't set cookies inside server components. - const verificationRequest = - await getUserEmailVerificationRequestFromRequest(); - if (verificationRequest === null && user.emailVerified) { - const redirectTo = await getRedirectCookie(); - return redirect(redirectTo); - } - - return ( - - Verify your email - - Enter the code sent to {verificationRequest?.email ?? user.email} - - - Change email - - - - - - ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx b/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx deleted file mode 100644 index 44ecc0af..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/resend-button.tsx +++ /dev/null @@ -1,37 +0,0 @@ -"use client"; - -import { Alert, Anchor, Button, Group, Stack, Text } from "@mantine/core"; -import { useAction } from "next-safe-action/hooks"; - -import { resendEmailVerificationAction } from "./actions"; - -export default function ResendButton() { - const action = useAction(resendEmailVerificationAction); - return ( - - - - {"Didn't receive the email?"} - - - - - {action.result.data?.message && ( - {action.result.data.message} - )} - - {action.result.data?.error && ( - - {action.result.data.error} - - )} - - ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts b/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts deleted file mode 100644 index 323bf7eb..00000000 --- a/packages/cli/template/extras/fmaddon-auth/app/auth/verify-email/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from "zod/v4"; - -export const emailVerificationSchema = z.object({ - code: z.string().length(8), -}); diff --git a/packages/cli/template/extras/fmaddon-auth/components/auth/actions.ts b/packages/cli/template/extras/fmaddon-auth/components/auth/actions.ts deleted file mode 100644 index f19dd156..00000000 --- a/packages/cli/template/extras/fmaddon-auth/components/auth/actions.ts +++ /dev/null @@ -1,19 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -import { - getCurrentSession, - invalidateSession, -} from "@/server/auth/utils/session"; - -export async function currentSessionAction() { - return await getCurrentSession(); -} - -export async function logoutAction() { - const { session } = await currentSessionAction(); - if (session) { - await invalidateSession(session.id); - } - redirect("/"); -} diff --git a/packages/cli/template/extras/fmaddon-auth/components/auth/protect.tsx b/packages/cli/template/extras/fmaddon-auth/components/auth/protect.tsx deleted file mode 100644 index 666efe74..00000000 --- a/packages/cli/template/extras/fmaddon-auth/components/auth/protect.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { getCurrentSession } from "@/server/auth/utils/session"; - -import AuthRedirect from "./redirect"; - -/** - * This server component will protect the contents of it's children from users who aren't logged in - * It will redirect to the login page if the user is not logged in, or the verify email page if the user is logged in but hasn't verified their email - */ -export default async function Protect({ - children, -}: { - children: React.ReactNode; -}) { - const { session, user } = await getCurrentSession(); - if (!session) return ; - if (!user.emailVerified) return ; - return <>{children}; -} diff --git a/packages/cli/template/extras/fmaddon-auth/components/auth/redirect.tsx b/packages/cli/template/extras/fmaddon-auth/components/auth/redirect.tsx deleted file mode 100644 index 8e3bb965..00000000 --- a/packages/cli/template/extras/fmaddon-auth/components/auth/redirect.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"use client"; - -import { Center, Loader } from "@mantine/core"; -import Cookies from "js-cookie"; -import { redirect } from "next/navigation"; -import { useEffect } from "react"; - -/** - * A client-side component that redirects to the given path, but saves the current path in the redirectTo cookie. - */ -export default function AuthRedirect({ path }: { path: string }) { - useEffect(() => { - if (typeof window !== "undefined") { - Cookies.set("redirectTo", window.location.pathname, { - expires: 1 / 24 / 60, // 1 hour - }); - redirect(path); - } - }, []); - - return ( -
- -
- ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/components/auth/use-user.ts b/packages/cli/template/extras/fmaddon-auth/components/auth/use-user.ts deleted file mode 100644 index 02d829cd..00000000 --- a/packages/cli/template/extras/fmaddon-auth/components/auth/use-user.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import type { Session } from "@/server/auth/utils/session"; -import type { User } from "@/server/auth/utils/user"; - -import { currentSessionAction, logoutAction } from "./actions"; - -type LogoutAction = () => Promise; -type UseUserResult = - | { - state: "authenticated"; - session: Session; - user: User; - logout: LogoutAction; - } - | { - state: "unauthenticated"; - session: null; - user: null; - logout: LogoutAction; - } - | { state: "loading"; session: null; user: null; logout: LogoutAction }; - -export function useUser(): UseUserResult { - const query = useQuery({ - queryKey: ["current-user"], - queryFn: () => currentSessionAction(), - retry: false, - }); - const queryClient = useQueryClient(); - - const { mutateAsync } = useMutation({ - mutationFn: logoutAction, - onMutate: async () => { - await queryClient.cancelQueries({ queryKey: ["current-user"] }); - queryClient.setQueryData(["current-user"], { session: null, user: null }); - }, - onSettled: () => - queryClient.invalidateQueries({ queryKey: ["current-user"] }), - }); - - const defaultResult: UseUserResult = { - state: "unauthenticated", - session: null, - user: null, - logout: mutateAsync, - }; - - if (query.isLoading) { - return { ...defaultResult, state: "loading" }; - } - if (query.data?.session) { - return { - ...defaultResult, - state: "authenticated", - session: query.data.session, - user: query.data.user, - }; - } - return defaultResult; -} diff --git a/packages/cli/template/extras/fmaddon-auth/components/auth/user-menu.tsx b/packages/cli/template/extras/fmaddon-auth/components/auth/user-menu.tsx deleted file mode 100644 index 06c99b7c..00000000 --- a/packages/cli/template/extras/fmaddon-auth/components/auth/user-menu.tsx +++ /dev/null @@ -1,52 +0,0 @@ -"use client"; - -import { Button, Menu, px, Skeleton } from "@mantine/core"; -import { IconChevronDown, IconLogout, IconUser } from "@tabler/icons-react"; -import Link from "next/link"; - -import { useUser } from "./use-user"; - -export default function UserMenu() { - const { state, session, user, logout } = useUser(); - - if (state === "loading") { - return ; - } - if (state === "unauthenticated") { - return ( - - ); - } - return ( - - - - - - } - > - My Profile - - - } - onClick={logout} - > - Sign out - - - - ); -} diff --git a/packages/cli/template/extras/fmaddon-auth/emails/auth-code.tsx b/packages/cli/template/extras/fmaddon-auth/emails/auth-code.tsx deleted file mode 100644 index 1e7191f2..00000000 --- a/packages/cli/template/extras/fmaddon-auth/emails/auth-code.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { - Body, - Container, - Head, - Heading, - Html, - Img, - Section, - Text, -} from "@react-email/components"; - -interface AuthCodeEmailProps { - validationCode: string; - type: "verification" | "password-reset"; -} - -export const AuthCodeEmail = ({ validationCode, type }: AuthCodeEmailProps) => ( - - - - - ProofKit - - {type === "verification" - ? "Verify Your Email" - : "Reset Your Password"} - - - Enter the following code to{" "} - {type === "verification" - ? "verify your email" - : "reset your password"} - -
- {validationCode} -
- - If you did not request this code, you can ignore this email. - -
- - -); - -AuthCodeEmail.PreviewProps = { - validationCode: "D7CU4GOV", - type: "verification", -} as AuthCodeEmailProps; - -export default AuthCodeEmail; - -const main = { - backgroundColor: "#ffffff", - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", -}; - -const container = { - backgroundColor: "#ffffff", - border: "1px solid #eee", - borderRadius: "5px", - boxShadow: "0 5px 10px rgba(20,50,70,.2)", - marginTop: "20px", - maxWidth: "360px", - margin: "0 auto", - padding: "68px 0 130px", -}; - -const logo: React.CSSProperties = { - margin: "0 auto", -}; - -const tertiary = { - color: "#0a85ea", - fontSize: "11px", - fontWeight: 700, - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", - height: "16px", - letterSpacing: "0", - lineHeight: "16px", - margin: "16px 8px 8px 8px", - textTransform: "uppercase" as const, - textAlign: "center" as const, -}; - -const secondary = { - color: "#000", - display: "inline-block", - fontFamily: "HelveticaNeue-Medium,Helvetica,Arial,sans-serif", - fontSize: "20px", - fontWeight: 500, - lineHeight: "24px", - marginBottom: "0", - marginTop: "0", - textAlign: "center" as const, - padding: "0 40px", -}; - -const codeContainer = { - background: "rgba(0,0,0,.05)", - borderRadius: "4px", - margin: "16px auto 14px", - verticalAlign: "middle", - width: "280px", -}; - -const code = { - color: "#000", - display: "inline-block", - fontFamily: "HelveticaNeue-Bold", - fontSize: "32px", - fontWeight: 700, - letterSpacing: "6px", - lineHeight: "40px", - paddingBottom: "8px", - paddingTop: "8px", - margin: "0 auto", - width: "100%", - textAlign: "center" as const, -}; - -const paragraph = { - color: "#444", - fontSize: "15px", - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", - letterSpacing: "0", - lineHeight: "23px", - padding: "0 40px", - margin: "0", - textAlign: "center" as const, -}; - -const link = { - color: "#444", - textDecoration: "underline", -}; - -const footer = { - color: "#000", - fontSize: "12px", - fontWeight: 800, - letterSpacing: "0", - lineHeight: "23px", - margin: "0", - marginTop: "20px", - fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", - textAlign: "center" as const, - textTransform: "uppercase" as const, -}; diff --git a/packages/cli/template/extras/fmaddon-auth/middleware.ts b/packages/cli/template/extras/fmaddon-auth/middleware.ts deleted file mode 100644 index b544bd32..00000000 --- a/packages/cli/template/extras/fmaddon-auth/middleware.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; - -export async function middleware(request: NextRequest): Promise { - if (request.method === "GET") { - const response = NextResponse.next(); - const token = request.cookies.get("session")?.value ?? null; - if (token !== null) { - // Only extend cookie expiration on GET requests since we can be sure - // a new session wasn't set when handling the request. - response.cookies.set("session", token, { - path: "/", - maxAge: 60 * 60 * 24 * 30, - sameSite: "lax", - httpOnly: true, - secure: process.env.NODE_ENV === "production", - }); - } - return response; - } - - const originHeader = request.headers.get("Origin"); - // NOTE: You may need to use `X-Forwarded-Host` instead - const hostHeader = request.headers.get("Host"); - if (originHeader === null || hostHeader === null) { - return new NextResponse(null, { - status: 403, - }); - } - let origin: URL; - try { - origin = new URL(originHeader); - } catch { - return new NextResponse(null, { - status: 403, - }); - } - if (origin.host !== hostHeader) { - return new NextResponse(null, { - status: 403, - }); - } - return NextResponse.next(); -} diff --git a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts b/packages/cli/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts deleted file mode 100644 index f04799d0..00000000 --- a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/email-verification.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { encodeBase32 } from "@oslojs/encoding"; -import { cookies } from "next/headers"; - -import { emailVerificationLayout } from "../db/client"; -import type { TemailVerification } from "../db/emailVerification"; -import { sendEmail } from "../email"; -import { generateRandomOTP } from "./index"; -import { getCurrentSession } from "./session"; - -/** - * An Email Verification Request is a record in the email verification table that is created when a user requests to change their email address. It's like a temporary session which can expire if the user doesn't verify the new email address within a certain amount of time. - */ - -/** - * Get a user's email verification request. - * @param userId - The ID of the user. - * @param id - The ID of the email verification request. - * @returns The email verification request, or null if it doesn't exist. - */ -export async function getUserEmailVerificationRequest( - userId: string, - id: string, -): Promise { - const result = await emailVerificationLayout.maybeFindFirst({ - query: { id_user: `==${userId}`, id: `==${id}` }, - }); - return result?.data.fieldData ?? null; -} - -/** - * Create a new email verification request for a user. - * @param id_user - The ID of the user. - * @param email - The email address to verify. - * @returns The email verification request. - */ -export async function createEmailVerificationRequest( - id_user: string, - email: string, -): Promise { - deleteUserEmailVerificationRequest(id_user); - const idBytes = new Uint8Array(20); - crypto.getRandomValues(idBytes); - const id = encodeBase32(idBytes).toLowerCase(); - - const code = generateRandomOTP(); - const expiresAt = new Date(Date.now() + 1000 * 60 * 10); - - const request: TemailVerification = { - id, - code, - expires_at: Math.floor(expiresAt.getTime() / 1000), - email, - id_user, - }; - - await emailVerificationLayout.create({ - fieldData: request, - }); - - return request; -} - -/** - * Delete a user's email verification request. - * @param id_user - The ID of the user. - */ -export async function deleteUserEmailVerificationRequest( - id_user: string, -): Promise { - const result = await emailVerificationLayout.maybeFindFirst({ - query: { id_user: `==${id_user}` }, - }); - if (result === null) return; - - await emailVerificationLayout.delete({ recordId: result.data.recordId }); -} - -/** - * Send a verification email to a user. - * @param email - The email address to send the verification email to. - * @param code - The verification code to send to the user. - */ -export async function sendVerificationEmail( - email: string, - code: string, -): Promise { - await sendEmail({ to: email, code, type: "verification" }); -} - -/** - * Set a cookie for a user's email verification request. - * @param request - The email verification request. - */ -export async function setEmailVerificationRequestCookie( - request: TemailVerification, -): Promise { - (await cookies()).set("email_verification", request.id, { - httpOnly: true, - path: "/", - secure: process.env.NODE_ENV === "production", - sameSite: "lax", - expires: request.expires_at - ? new Date(request.expires_at * 1000) - : new Date(Date.now() + 1000 * 60 * 60), - }); -} - -/** - * Delete the cookie for a user's email verification request. - */ -export async function deleteEmailVerificationRequestCookie(): Promise { - (await cookies()).set("email_verification", "", { - httpOnly: true, - path: "/", - secure: process.env.NODE_ENV === "production", - sameSite: "lax", - maxAge: 0, - }); -} - -/** - * Get a user's email verification request from the cookie. - * @returns The email verification request, or null if it doesn't exist. - */ -export async function getUserEmailVerificationRequestFromRequest(): Promise { - const { user } = await getCurrentSession(); - if (user === null) { - return null; - } - const id = (await cookies()).get("email_verification")?.value ?? null; - if (id === null) { - return null; - } - const request = await getUserEmailVerificationRequest(user.id, id); - - return request; -} diff --git a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/encryption.ts b/packages/cli/template/extras/fmaddon-auth/server/auth/utils/encryption.ts deleted file mode 100644 index 2ba22737..00000000 --- a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/encryption.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { DynamicBuffer } from "@oslojs/binary"; -import { decodeBase64 } from "@oslojs/encoding"; -import { createCipheriv, createDecipheriv } from "crypto"; - -const key = decodeBase64(process.env.ENCRYPTION_KEY ?? ""); - -export function encrypt(data: Uint8Array): Uint8Array { - const iv = new Uint8Array(16); - crypto.getRandomValues(iv); - const cipher = createCipheriv("aes-128-gcm", key, iv); - const encrypted = new DynamicBuffer(0); - encrypted.write(iv); - encrypted.write(cipher.update(data)); - encrypted.write(cipher.final()); - encrypted.write(cipher.getAuthTag()); - return encrypted.bytes(); -} - -/** - * Encrypt a string for storage in the database. - * Here we're returning a base64 encoded string since FileMaker doesn't store binary data. - * @param data - The string to encrypt. - * @returns The encrypted string. - */ -export function encryptString(data: string): string { - const encrypted = encrypt(new TextEncoder().encode(data)); - return Buffer.from(encrypted).toString("base64"); -} - -/** - * Decrypt a string stored in the database. - * @param encrypted - The encrypted string to decrypt. - * @returns The decrypted string. - */ -export function decrypt(encrypted: Uint8Array): Uint8Array { - if (encrypted.byteLength < 33) { - throw new Error("Invalid data"); - } - const decipher = createDecipheriv("aes-128-gcm", key, encrypted.slice(0, 16)); - decipher.setAuthTag(encrypted.slice(encrypted.byteLength - 16)); - const decrypted = new DynamicBuffer(0); - decrypted.write( - decipher.update(encrypted.slice(16, encrypted.byteLength - 16)), - ); - decrypted.write(decipher.final()); - return decrypted.bytes(); -} - -export function decryptToString(data: Uint8Array): string { - return new TextDecoder().decode(decrypt(data)); -} diff --git a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/index.ts b/packages/cli/template/extras/fmaddon-auth/server/auth/utils/index.ts deleted file mode 100644 index 13f64ce3..00000000 --- a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { encodeBase32UpperCaseNoPadding } from "@oslojs/encoding"; - -export function generateRandomOTP(): string { - const bytes = new Uint8Array(5); - crypto.getRandomValues(bytes); - const code = encodeBase32UpperCaseNoPadding(bytes); - return code; -} - -export const options = { - password: { - minLength: 8, - maxLength: 255, - checkCompromised: false, // set to true to prevent known compromised passwords on signup - }, -}; diff --git a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts b/packages/cli/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts deleted file mode 100644 index 38f7bf3d..00000000 --- a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/password-reset.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { sha256 } from "@oslojs/crypto/sha2"; -import { encodeHexLowerCase } from "@oslojs/encoding"; -import { cookies } from "next/headers"; - -import { passwordResetLayout } from "../db/client"; -import type { TpasswordReset } from "../db/passwordReset"; -import { sendEmail } from "../email"; -import { generateRandomOTP } from "./index"; -import type { User } from "./user"; - -type PasswordResetSession = Omit< - TpasswordReset, - | "proofkit_auth_users::email" - | "proofkit_auth_users::emailVerified" - | "proofkit_auth_users::username" ->; - -export async function createPasswordResetSession( - token: string, - id_user: string, - email: string, -): Promise { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const session: PasswordResetSession = { - id: sessionId, - id_user, - email, - expires_at: Math.floor( - new Date(Date.now() + 1000 * 60 * 10).getTime() / 1000, - ), - code: generateRandomOTP(), - email_verified: 0, - }; - await passwordResetLayout.create({ fieldData: session }); - - return session; -} - -/** - * Validate a password reset session token. - * @param token - The password reset session token. - * @returns The password reset session, or null if it doesn't exist. - */ -export async function validatePasswordResetSessionToken( - token: string, -): Promise { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const row = await passwordResetLayout.maybeFindFirst({ - query: { id: `==${sessionId}` }, - }); - - if (row === null) { - return { session: null, user: null }; - } - const session: PasswordResetSession = { - id: row.data.fieldData.id, - id_user: row.data.fieldData.id_user, - email: row.data.fieldData.email, - code: row.data.fieldData.code, - expires_at: row.data.fieldData.expires_at, - email_verified: row.data.fieldData.email_verified, - }; - - const user: User = { - id: row.data.fieldData.id_user, - email: row.data.fieldData["proofkit_auth_users::email"], - username: row.data.fieldData["proofkit_auth_users::username"], - emailVerified: Boolean( - row.data.fieldData["proofkit_auth_users::emailVerified"], - ), - }; - if (session.expires_at && Date.now() >= session.expires_at * 1000) { - await passwordResetLayout.delete({ recordId: row.data.recordId }); - return { session: null, user: null }; - } - return { session, user }; -} - -async function fetchPasswordResetSession(sessionId: string) { - return ( - await passwordResetLayout.findOne({ query: { id: `==${sessionId}` } }) - ).data; -} - -export async function setPasswordResetSessionAsEmailVerified( - sessionId: string, -): Promise { - const { recordId } = await fetchPasswordResetSession(sessionId); - await passwordResetLayout.update({ - recordId, - fieldData: { email_verified: 1 }, - }); -} - -export async function invalidateUserPasswordResetSessions( - userId: string, -): Promise { - const sessions = await passwordResetLayout.find({ - query: { id_user: `==${userId}` }, - ignoreEmptyResult: true, - }); - for (const session of sessions.data) { - await passwordResetLayout.delete({ recordId: session.recordId }); - } -} - -export async function validatePasswordResetSessionRequest(): Promise { - const token = (await cookies()).get("password_reset_session")?.value ?? null; - if (token === null) { - return { session: null, user: null }; - } - const result = await validatePasswordResetSessionToken(token); - if (result.session === null) { - deletePasswordResetSessionTokenCookie(); - } - return result; -} - -export async function setPasswordResetSessionTokenCookie( - token: string, - expiresAt: number | null, -): Promise { - (await cookies()).set("password_reset_session", token, { - expires: expiresAt - ? new Date(expiresAt * 1000) - : new Date(Date.now() + 60 * 60 * 1000), - sameSite: "lax", - httpOnly: true, - path: "/", - secure: process.env.NODE_ENV === "production", - }); -} - -export async function deletePasswordResetSessionTokenCookie(): Promise { - (await cookies()).set("password_reset_session", "", { - maxAge: 0, - sameSite: "lax", - httpOnly: true, - path: "/", - secure: process.env.NODE_ENV === "production", - }); -} - -export async function sendPasswordResetEmail( - email: string, - code: string, -): Promise { - await sendEmail({ to: email, code, type: "password-reset" }); -} - -export type PasswordResetSessionValidationResult = - | { session: PasswordResetSession; user: User } - | { session: null; user: null }; diff --git a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/password.ts b/packages/cli/template/extras/fmaddon-auth/server/auth/utils/password.ts deleted file mode 100644 index e637db4d..00000000 --- a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/password.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { hash, verify } from "@node-rs/argon2"; -import { sha1 } from "@oslojs/crypto/sha1"; -import { encodeHexLowerCase } from "@oslojs/encoding"; -import { options } from "."; - -/** - * Hash a password using Argon2. - * @param password - The password to hash. - * @returns The hashed password. - */ -export async function hashPassword(password: string): Promise { - return await hash(password, { - memoryCost: 19456, - timeCost: 2, - outputLen: 32, - parallelism: 1, - }); -} - -/** - * Verify that a password matches a hash. - * @param hash - The hash to verify against. - * @param password - The password to verify. - * @returns True if the password matches the hash, false otherwise. - */ -export async function verifyPasswordHash( - hash: string, - password: string, -): Promise { - return await verify(hash, password); -} - -/** - * Verify that a password is strong enough. - * @param password - The password to verify. - * @returns True if the password is strong enough, false otherwise. - */ -export async function verifyPasswordStrength( - password: string, -): Promise { - if ( - password.length < options.password.minLength || - password.length > options.password.maxLength - ) { - return false; - } - - if (options.password.checkCompromised) { - const hash = encodeHexLowerCase(sha1(new TextEncoder().encode(password))); - const hashPrefix = hash.slice(0, 5); - const response = await fetch( - `https://api.pwnedpasswords.com/range/${hashPrefix}`, - ); - const data = await response.text(); - const items = data.split("\n"); - for (const item of items) { - const hashSuffix = item.slice(0, 35).toLowerCase(); - if (hash === hashPrefix + hashSuffix) { - console.log( - "User's new password was found in list of compromised passwords, reject", - ); - return false; - } - } - } - return true; -} diff --git a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/redirect.ts b/packages/cli/template/extras/fmaddon-auth/server/auth/utils/redirect.ts deleted file mode 100644 index 4655871e..00000000 --- a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/redirect.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { cookies } from "next/headers"; - -export async function getRedirectCookie() { - const cookieStore = await cookies(); - const redirectTo = cookieStore.get("redirectTo")?.value; - cookieStore.delete("redirectTo"); - return redirectTo ?? "/"; -} diff --git a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/session.ts b/packages/cli/template/extras/fmaddon-auth/server/auth/utils/session.ts deleted file mode 100644 index 7b69d061..00000000 --- a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/session.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { sha256 } from "@oslojs/crypto/sha2"; -import { - encodeBase32LowerCaseNoPadding, - encodeHexLowerCase, -} from "@oslojs/encoding"; -import { cookies } from "next/headers"; -import { cache } from "react"; - -import { sessionsLayout } from "../db/client"; -import { Tsessions as _Session } from "../db/sessions"; -import type { User } from "./user"; - -/** - * Generate a random session token with sufficient entropy for a session ID. - * @returns The session token. - */ -export function generateSessionToken(): string { - const bytes = new Uint8Array(20); - crypto.getRandomValues(bytes); - const token = encodeBase32LowerCaseNoPadding(bytes); - return token; -} - -/** - * Create a new session for a user and save it to the database. - * @param token - The session token. - * @param userId - The ID of the user. - * @returns The session. - */ -export async function createSession( - token: string, - userId: string, -): Promise { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const session: Session = { - id: sessionId, - id_user: userId, - expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), - }; - - // create session in DB - await sessionsLayout.create({ - fieldData: { - id: session.id, - id_user: session.id_user, - expiresAt: Math.floor(session.expiresAt.getTime() / 1000), - }, - }); - - return session; -} - -/** - * Invalidate a session by deleting it from the database. - * @param sessionId - The ID of the session to invalidate. - */ -export async function invalidateSession(sessionId: string): Promise { - const fmResult = await sessionsLayout.maybeFindFirst({ - query: { id: `==${sessionId}` }, - }); - if (fmResult === null) { - return; - } - await sessionsLayout.delete({ recordId: fmResult.data.recordId }); -} - -/** - * Validate a session token to make sure it still exists in the database and hasn't expired. - * @param token - The session token. - * @returns The session, or null if it doesn't exist. - */ -export async function validateSessionToken( - token: string, -): Promise { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - - const result = await sessionsLayout.maybeFindFirst({ - query: { id: `==${sessionId}` }, - }); - if (result === null) { - return { session: null, user: null }; - } - - const fmResult = result.data.fieldData; - const recordId = result.data.recordId; - const session: Session = { - id: fmResult.id, - id_user: fmResult.id_user, - expiresAt: fmResult.expiresAt - ? new Date(fmResult.expiresAt * 1000) - : new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), - }; - - const user: User = { - id: session.id_user, - email: fmResult["proofkit_auth_users::email"], - emailVerified: Boolean(fmResult["proofkit_auth_users::emailVerified"]), - username: fmResult["proofkit_auth_users::username"], - }; - - // delete session if it has expired - if (Date.now() >= session.expiresAt.getTime()) { - await sessionsLayout.delete({ recordId }); - return { session: null, user: null }; - } - - // extend session if it's going to expire soon - // You may want to customize this logic to better suit your app's requirements - if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) { - session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30); - await sessionsLayout.update({ - recordId, - fieldData: { - expiresAt: Math.floor(session.expiresAt.getTime() / 1000), - }, - }); - } - - return { session, user }; -} - -/** - * Get the current session from the cookie. - * Wrapped in a React cache to avoid calling the database more than once per request - * This function can be used in server components, server actions, and route handlers (but importantly not middleware). - * @returns The session, or null if it doesn't exist. - */ -export const getCurrentSession = cache( - async (): Promise => { - const token = (await cookies()).get("session")?.value ?? null; - if (token === null) { - return { session: null, user: null }; - } - const result = await validateSessionToken(token); - return result; - }, -); - -/** - * Invalidate all sessions for a user by deleting them from the database. - * @param userId - The ID of the user. - */ -export async function invalidateUserSessions(userId: string): Promise { - const sessions = await sessionsLayout.findAll({ - query: { id_user: `==${userId}` }, - }); - for (const session of sessions) { - await sessionsLayout.delete({ recordId: session.recordId }); - } -} - -/** - * Set a cookie for a session. - * @param token - The session token. - * @param expiresAt - The expiration date of the session. - */ -export async function setSessionTokenCookie( - token: string, - expiresAt: Date, -): Promise { - (await cookies()).set("session", token, { - httpOnly: true, - path: "/", - secure: process.env.NODE_ENV === "production", - sameSite: "lax", - expires: expiresAt, - }); -} - -/** - * Delete the session cookie. - */ -export async function deleteSessionTokenCookie(): Promise { - (await cookies()).set("session", "", { - httpOnly: true, - path: "/", - secure: process.env.NODE_ENV === "production", - sameSite: "lax", - maxAge: 0, - }); -} - -export interface Session { - id: string; - expiresAt: Date; - id_user: string; -} - -type SessionValidationResult = - | { session: Session; user: User } - | { session: null; user: null }; diff --git a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/user.ts b/packages/cli/template/extras/fmaddon-auth/server/auth/utils/user.ts deleted file mode 100644 index 3ed77da2..00000000 --- a/packages/cli/template/extras/fmaddon-auth/server/auth/utils/user.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { usersLayout } from "../db/client"; -import type { Tusers as _User } from "../db/users"; -import { hashPassword, verifyPasswordHash } from "./password"; - -export type User = Partial< - Omit<_User, "id" | "password_hash" | "recovery_code" | "emailVerified"> -> & { - id: string; - email: string; - emailVerified: boolean; -}; - -/** An internal helper function to fetch a user from the database. */ -async function fetchUser(userId: string) { - const { data } = await usersLayout.findOne({ - query: { id: `==${userId}` }, - }); - return data; -} - -/** Create a new user in the database. */ -export async function createUser( - email: string, - password: string, -): Promise { - const password_hash = await hashPassword(password); - const { recordId } = await usersLayout.create({ - fieldData: { - email, - password_hash, - emailVerified: 0, - }, - }); - const fmResult = await usersLayout.get({ recordId }); - const { fieldData } = fmResult.data[0]; - - const user: User = { - id: fieldData.id, - email, - emailVerified: false, - username: "", - }; - return user; -} - -/** Update a user's password in the database. */ -export async function updateUserPassword( - userId: string, - password: string, -): Promise { - const password_hash = await hashPassword(password); - const { recordId } = await fetchUser(userId); - - await usersLayout.update({ recordId, fieldData: { password_hash } }); -} - -export async function updateUserEmailAndSetEmailAsVerified( - userId: string, - email: string, -): Promise { - const { recordId } = await fetchUser(userId); - await usersLayout.update({ - recordId, - fieldData: { email, emailVerified: 1 }, - }); -} - -export async function setUserAsEmailVerifiedIfEmailMatches( - userId: string, - email: string, -): Promise { - try { - const { - data: { recordId }, - } = await usersLayout.findOne({ - query: { id: `==${userId}`, email: `==${email}` }, - }); - await usersLayout.update({ recordId, fieldData: { emailVerified: 1 } }); - return true; - } catch (error) { - return false; - } -} - -export async function getUserFromEmail(email: string): Promise { - const fmResult = await usersLayout.maybeFindFirst({ - query: { email: `==${email}` }, - }); - if (fmResult === null) return null; - - const { - data: { fieldData }, - } = fmResult; - - const user: User = { - id: fieldData.id, - email: fieldData.email, - emailVerified: Boolean(fieldData.emailVerified), - username: fieldData.username, - }; - return user; -} - -/** - * Validate a user's email/password combination. - * @param email - The user's email. - * @param password - The user's password. - * @returns The user, or null if the login is invalid. - */ -export async function validateLogin( - email: string, - password: string, -): Promise { - try { - const { - data: { fieldData }, - } = await usersLayout.findOne({ - query: { email: `==${email}` }, - }); - - const validPassword = await verifyPasswordHash( - fieldData.password_hash, - password, - ); - if (!validPassword) { - return null; - } - const user: User = { - id: fieldData.id, - email: fieldData.email, - emailVerified: Boolean(fieldData.emailVerified), - username: fieldData.username, - }; - return user; - } catch (error) { - return null; - } -} - -export async function checkEmailAvailability(email: string): Promise { - const { data } = await usersLayout.find({ - query: { email: `==${email}` }, - ignoreEmptyResult: true, - }); - return data.length === 0; -} diff --git a/packages/cli/template/extras/prisma/schema/base-planetscale.prisma b/packages/cli/template/extras/prisma/schema/base-planetscale.prisma deleted file mode 100644 index 6b9dd139..00000000 --- a/packages/cli/template/extras/prisma/schema/base-planetscale.prisma +++ /dev/null @@ -1,24 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" - previewFeatures = ["driverAdapters"] -} - -datasource db { - provider = "mysql" - url = env("DATABASE_URL") - - // If you have enabled foreign key constraints for your database, remove this line. - relationMode = "prisma" -} - -model Post { - id Int @id @default(autoincrement()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([name]) -} diff --git a/packages/cli/template/extras/prisma/schema/base.prisma b/packages/cli/template/extras/prisma/schema/base.prisma deleted file mode 100644 index ddb6e099..00000000 --- a/packages/cli/template/extras/prisma/schema/base.prisma +++ /dev/null @@ -1,20 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "sqlite" - url = env("DATABASE_URL") -} - -model Post { - id Int @id @default(autoincrement()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([name]) -} diff --git a/packages/cli/template/extras/prisma/schema/with-auth-planetscale.prisma b/packages/cli/template/extras/prisma/schema/with-auth-planetscale.prisma deleted file mode 100644 index 198915b9..00000000 --- a/packages/cli/template/extras/prisma/schema/with-auth-planetscale.prisma +++ /dev/null @@ -1,77 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" - previewFeatures = ["driverAdapters"] -} - -datasource db { - provider = "mysql" - url = env("DATABASE_URL") - - // If you have enabled foreign key constraints for your database, remove this line. - relationMode = "prisma" -} - -model Post { - id Int @id @default(autoincrement()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - createdBy User @relation(fields: [createdById], references: [id]) - createdById String - - @@index([name]) - @@index([createdById]) -} - -// Necessary for Next auth -model Account { - id String @id @default(cuid()) - userId String - type String - provider String - providerAccountId String - refresh_token String? @db.Text - access_token String? @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? @db.Text - session_state String? - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([provider, providerAccountId]) - @@index([userId]) -} - -model Session { - id String @id @default(cuid()) - sessionToken String @unique - userId String - expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@index([userId]) -} - -model User { - id String @id @default(cuid()) - name String? - email String? @unique - emailVerified DateTime? - image String? - accounts Account[] - sessions Session[] - posts Post[] -} - -model VerificationToken { - identifier String - token String @unique - expires DateTime - - @@unique([identifier, token]) -} diff --git a/packages/cli/template/extras/prisma/schema/with-auth.prisma b/packages/cli/template/extras/prisma/schema/with-auth.prisma deleted file mode 100644 index b17831e6..00000000 --- a/packages/cli/template/extras/prisma/schema/with-auth.prisma +++ /dev/null @@ -1,74 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "sqlite" - // NOTE: When using mysql or sqlserver, uncomment the @db.Text annotations in model Account below - // Further reading: - // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema - // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string - url = env("DATABASE_URL") -} - -model Post { - id Int @id @default(autoincrement()) - name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - createdBy User @relation(fields: [createdById], references: [id]) - createdById String - - @@index([name]) -} - -// Necessary for Next auth -model Account { - id String @id @default(cuid()) - userId String - type String - provider String - providerAccountId String - refresh_token String? // @db.Text - access_token String? // @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? // @db.Text - session_state String? - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - refresh_token_expires_in Int? - - @@unique([provider, providerAccountId]) -} - -model Session { - id String @id @default(cuid()) - sessionToken String @unique - userId String - expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) -} - -model User { - id String @id @default(cuid()) - name String? - email String? @unique - emailVerified DateTime? - image String? - accounts Account[] - sessions Session[] - posts Post[] -} - -model VerificationToken { - identifier String - token String @unique - expires DateTime - - @@unique([identifier, token]) -} diff --git a/packages/cli/template/extras/src/app/_components/post-tw.tsx b/packages/cli/template/extras/src/app/_components/post-tw.tsx deleted file mode 100644 index fabff895..00000000 --- a/packages/cli/template/extras/src/app/_components/post-tw.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"use client"; - -import { useState } from "react"; - -import { api } from "~/trpc/react"; - -export function LatestPost() { - const [latestPost] = api.post.getLatest.useSuspenseQuery(); - - const utils = api.useUtils(); - const [name, setName] = useState(""); - const createPost = api.post.create.useMutation({ - onSuccess: async () => { - await utils.post.invalidate(); - setName(""); - }, - }); - - return ( -
- {latestPost ? ( -

Your most recent post: {latestPost.name}

- ) : ( -

You have no posts yet.

- )} -
{ - e.preventDefault(); - createPost.mutate({ name }); - }} - className="flex flex-col gap-2" - > - setName(e.target.value)} - className="w-full rounded-full px-4 py-2 text-black" - /> - -
-
- ); -} diff --git a/packages/cli/template/extras/src/app/_components/post.tsx b/packages/cli/template/extras/src/app/_components/post.tsx deleted file mode 100644 index 56235b0c..00000000 --- a/packages/cli/template/extras/src/app/_components/post.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import { useState } from "react"; - -import { api } from "~/trpc/react"; -import styles from "../index.module.css"; - -export function LatestPost() { - const [latestPost] = api.post.getLatest.useSuspenseQuery(); - - const utils = api.useUtils(); - const [name, setName] = useState(""); - const createPost = api.post.create.useMutation({ - onSuccess: async () => { - await utils.post.invalidate(); - setName(""); - }, - }); - - return ( -
- {latestPost ? ( -

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

- ) : ( -

You have no posts yet.

- )} - -
{ - e.preventDefault(); - createPost.mutate({ name }); - }} - className={styles.form} - > - setName(e.target.value)} - className={styles.input} - /> - -
-
- ); -} diff --git a/packages/cli/template/extras/src/app/api/auth/[...nextauth]/route.ts b/packages/cli/template/extras/src/app/api/auth/[...nextauth]/route.ts deleted file mode 100644 index 6112167f..00000000 --- a/packages/cli/template/extras/src/app/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { handlers } from "@/server/auth"; // Referring to the auth.ts we just created - -export const { GET, POST } = handlers; diff --git a/packages/cli/template/extras/src/app/api/trpc/[trpc]/route.ts b/packages/cli/template/extras/src/app/api/trpc/[trpc]/route.ts deleted file mode 100644 index 0658508b..00000000 --- a/packages/cli/template/extras/src/app/api/trpc/[trpc]/route.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; -import type { NextRequest } from "next/server"; - -import { env } from "~/env"; -import { appRouter } from "~/server/api/root"; -import { createTRPCContext } from "~/server/api/trpc"; - -/** - * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when - * handling a HTTP request (e.g. when you make requests from Client Components). - */ -const createContext = async (req: NextRequest) => { - return createTRPCContext({ - headers: req.headers, - }); -}; - -const handler = (req: NextRequest) => - fetchRequestHandler({ - endpoint: "/api/trpc", - req, - router: appRouter, - createContext: () => createContext(req), - onError: - env.NODE_ENV === "development" - ? ({ path, error }) => { - console.error( - `โŒ tRPC failed on ${path ?? ""}: ${error.message}`, - ); - } - : undefined, - }); - -export { handler as GET, handler as POST }; diff --git a/packages/cli/template/extras/src/app/clerk-auth/layout.tsx b/packages/cli/template/extras/src/app/clerk-auth/layout.tsx deleted file mode 100644 index e74f0b09..00000000 --- a/packages/cli/template/extras/src/app/clerk-auth/layout.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Center } from "@mantine/core"; -import type React from "react"; - -export default function AuthLayout({ - children, -}: { - children: React.ReactNode; -}) { - return
{children}
; -} diff --git a/packages/cli/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx b/packages/cli/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx deleted file mode 100644 index 405f94c3..00000000 --- a/packages/cli/template/extras/src/app/clerk-auth/signin/[[...sign-in]]/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { SignIn } from "@clerk/nextjs"; - -export default function Page() { - return ; -} diff --git a/packages/cli/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx b/packages/cli/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx deleted file mode 100644 index 4b651293..00000000 --- a/packages/cli/template/extras/src/app/clerk-auth/signup/[[...sign-up]]/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { SignUp } from "@clerk/nextjs"; - -export default function Page() { - return ; -} diff --git a/packages/cli/template/extras/src/app/layout/base.tsx b/packages/cli/template/extras/src/app/layout/base.tsx deleted file mode 100644 index b1748540..00000000 --- a/packages/cli/template/extras/src/app/layout/base.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { ColorSchemeScript, MantineProvider } from "@mantine/core"; -import { ModalsProvider } from "@mantine/modals"; -import { Notifications } from "@mantine/notifications"; - -import "@mantine/core/styles.css"; -import "@mantine/notifications/styles.css"; -import "@mantine/dates/styles.css"; -import "mantine-react-table/styles.css"; - -import type { Metadata } from "next"; - -export const metadata: Metadata = { - title: "My ProofKit App", - description: "Generated by proofkit", - icons: [{ rel: "icon", url: "/favicon.ico" }], -}; - -export default function RootLayout({ - children, -}: Readonly<{ children: React.ReactNode }>) { - return ( - - - - - - - - {children} - - - - ); -} diff --git a/packages/cli/template/extras/src/app/layout/main-shell.tsx b/packages/cli/template/extras/src/app/layout/main-shell.tsx deleted file mode 100644 index f561571a..00000000 --- a/packages/cli/template/extras/src/app/layout/main-shell.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { - AppShell, - AppShellFooter, - AppShellHeader, - AppShellMain, - AppShellNavbar, -} from "@mantine/core"; -import type React from "react"; - -/** Layout configuration Edit these values to change the layout */ -export const showHeader = false; -export const showFooter = false; -export const showLeftNavbar = false; - -export const headerHeight = 60; -export const footerHeight = 60; -export const leftNavbarWidth = 200; - -export default function Layout({ children }: { children: React.ReactNode }) { - return ( - - {showHeader && Header} - {showLeftNavbar && Left Navbar} - {children} - {showFooter && Footer} - - ); -} diff --git a/packages/cli/template/extras/src/app/layout/with-trpc-tw.tsx b/packages/cli/template/extras/src/app/layout/with-trpc-tw.tsx deleted file mode 100644 index 9c0b2a5d..00000000 --- a/packages/cli/template/extras/src/app/layout/with-trpc-tw.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import "~/styles/globals.css"; - -import { GeistSans } from "geist/font/sans"; -import type { Metadata } from "next"; - -import { TRPCReactProvider } from "~/trpc/react"; - -export const metadata: Metadata = { - title: "Create T3 App", - description: "Generated by proofkit", - icons: [{ rel: "icon", url: "/favicon.ico" }], -}; - -export default function RootLayout({ - children, -}: Readonly<{ children: React.ReactNode }>) { - return ( - - - {children} - - - ); -} diff --git a/packages/cli/template/extras/src/app/layout/with-trpc.tsx b/packages/cli/template/extras/src/app/layout/with-trpc.tsx deleted file mode 100644 index 54285b9d..00000000 --- a/packages/cli/template/extras/src/app/layout/with-trpc.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import "~/styles/globals.css"; - -import { GeistSans } from "geist/font/sans"; -import type { Metadata } from "next"; - -import { TRPCReactProvider } from "~/trpc/react"; - -export const metadata: Metadata = { - title: "Create T3 App", - description: "Generated by proofkit", - icons: [{ rel: "icon", url: "/favicon.ico" }], -}; - -export default function RootLayout({ - children, -}: Readonly<{ children: React.ReactNode }>) { - return ( - - - {children} - - - ); -} diff --git a/packages/cli/template/extras/src/app/layout/with-tw.tsx b/packages/cli/template/extras/src/app/layout/with-tw.tsx deleted file mode 100644 index fdc321da..00000000 --- a/packages/cli/template/extras/src/app/layout/with-tw.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import "~/styles/globals.css"; - -import { GeistSans } from "geist/font/sans"; -import type { Metadata } from "next"; - -export const metadata: Metadata = { - title: "Create T3 App", - description: "Generated by proofkit", - icons: [{ rel: "icon", url: "/favicon.ico" }], -}; - -export default function RootLayout({ - children, -}: Readonly<{ children: React.ReactNode }>) { - return ( - - {children} - - ); -} diff --git a/packages/cli/template/extras/src/app/next-auth/layout.tsx b/packages/cli/template/extras/src/app/next-auth/layout.tsx deleted file mode 100644 index f87b5175..00000000 --- a/packages/cli/template/extras/src/app/next-auth/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Card, Center } from "@mantine/core"; -import { redirect } from "next/navigation"; -import type React from "react"; -import { auth } from "@/server/auth"; - -export default async function Layout({ - children, -}: { - children: React.ReactNode; -}) { - const session = await auth(); - if (session) { - return redirect("/"); - } - return ( -
- - {children} - -
- ); -} diff --git a/packages/cli/template/extras/src/app/next-auth/signin/page.tsx b/packages/cli/template/extras/src/app/next-auth/signin/page.tsx deleted file mode 100644 index 82ef5eff..00000000 --- a/packages/cli/template/extras/src/app/next-auth/signin/page.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { - Button, - Card, - Divider, - PasswordInput, - Stack, - Text, - TextInput, -} from "@mantine/core"; -import Link from "next/link"; -import { redirect } from "next/navigation"; -import { AuthError } from "next-auth"; -import { providerMap, signIn } from "@/server/auth"; - -export default async function SignInPage(props: { - searchParams: Promise<{ callbackUrl: string | undefined }>; -}) { - const searchParams = await props.searchParams; - return ( - -
{ - "use server"; - try { - await signIn("credentials", formData); - } catch (error) { - if (error instanceof AuthError) { - return redirect(`/auth/signin?error=${error.type}`); - } - throw error; - } - }} - > - - - - - - -
- {providerMap.length > 0 && ( - <> - - {Object.values(providerMap).map((provider) => ( -
{ - "use server"; - try { - await signIn(provider.id, { - redirectTo: searchParams.callbackUrl ?? "", - }); - } catch (error) { - // Signin can fail for a number of reasons, such as the user - // not existing, or the user not having the correct role. - // In some cases, you may want to redirect to a custom error - if (error instanceof AuthError) { - return redirect(`/auth/signin?error=${error.type}`); - } - - // Otherwise if a redirects happens Next.js can handle it - // so you can just re-thrown the error and let Next.js handle it. - // Docs: - // https://nextjs.org/docs/app/api-reference/functions/redirect#server-component - throw error; - } - }} - > - -
- ))} - - )} - - - {"Don't have an account? "} - Sign up - -
- ); -} diff --git a/packages/cli/template/extras/src/app/next-auth/signup/action.ts b/packages/cli/template/extras/src/app/next-auth/signup/action.ts deleted file mode 100644 index ad7ce394..00000000 --- a/packages/cli/template/extras/src/app/next-auth/signup/action.ts +++ /dev/null @@ -1,24 +0,0 @@ -"use server"; - -import { signIn } from "@/server/auth"; -import { userSignUp } from "@/server/data/users"; -import { actionClient } from "@/server/safe-action"; - -import { signUpSchema } from "./validation"; - -export const signUpAction = actionClient - .schema(signUpSchema) - .action(async ({ parsedInput, ctx }) => { - const { email, password } = parsedInput; - - await userSignUp({ email, password }); - - await signIn("credentials", { - email, - password, - }); - - return { - success: true, - }; - }); diff --git a/packages/cli/template/extras/src/app/next-auth/signup/page.tsx b/packages/cli/template/extras/src/app/next-auth/signup/page.tsx deleted file mode 100644 index f87909c5..00000000 --- a/packages/cli/template/extras/src/app/next-auth/signup/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { Button, PasswordInput, Stack, Text, TextInput } from "@mantine/core"; -import { useHookFormAction } from "@next-safe-action/adapter-react-hook-form/hooks"; -import Link from "next/link"; -import React from "react"; - -import { signUpAction } from "./action"; -import { signUpSchema } from "./validation"; - -export default function SignUpPage(props: { - searchParams: Promise<{ callbackUrl: string | undefined }>; -}) { - const { form, action, handleSubmitWithAction, resetFormAndAction } = - useHookFormAction(signUpAction, zodResolver(signUpSchema), { - actionProps: {}, - formProps: {}, - errorMapProps: {}, - }); - - return ( - -
- - - - - - -
- - Already have an account? Sign in - -
- ); -} diff --git a/packages/cli/template/extras/src/app/next-auth/signup/validation.ts b/packages/cli/template/extras/src/app/next-auth/signup/validation.ts deleted file mode 100644 index 58ef8d09..00000000 --- a/packages/cli/template/extras/src/app/next-auth/signup/validation.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { z } from "zod/v4"; - -export const signUpSchema = z - .object({ - email: z.string().email(), - password: z.string(), - passwordConfirm: z.string(), - }) - .refine((data) => data.password === data.passwordConfirm, { - message: "Passwords don't match", - path: ["passwordConfirm"], - }); diff --git a/packages/cli/template/extras/src/app/page/base.tsx b/packages/cli/template/extras/src/app/page/base.tsx deleted file mode 100644 index b40d0174..00000000 --- a/packages/cli/template/extras/src/app/page/base.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { Text } from "@mantine/core"; -import Link from "next/link"; - -export default function Home() { - return Welcome!; -} diff --git a/packages/cli/template/extras/src/app/page/with-auth-trpc-tw.tsx b/packages/cli/template/extras/src/app/page/with-auth-trpc-tw.tsx deleted file mode 100644 index c0c08909..00000000 --- a/packages/cli/template/extras/src/app/page/with-auth-trpc-tw.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import Link from "next/link"; - -import { LatestPost } from "~/app/_components/post"; -import { getServerAuthSession } from "~/server/auth"; -import { api, HydrateClient } from "~/trpc/server"; - -export default async function Home() { - const hello = await api.post.hello({ text: "from tRPC" }); - const session = await getServerAuthSession(); - - void api.post.getLatest.prefetch(); - - return ( - -
-
-

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

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

- -
-

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

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

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

- -
-

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

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

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

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

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

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

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

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

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

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

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

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

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

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

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

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

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

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

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

- Create T3 App -

-
- -

First Steps โ†’

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

Documentation โ†’

-
- Learn more about Create T3 App, the libraries it uses, and how - to deploy it. -
- -
-
-
- - ); -} diff --git a/packages/cli/template/extras/src/server/api/root.ts b/packages/cli/template/extras/src/server/api/root.ts deleted file mode 100644 index 374285c1..00000000 --- a/packages/cli/template/extras/src/server/api/root.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { postRouter } from "~/server/api/routers/post"; -import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc"; - -/** - * This is the primary router for your server. - * - * All routers added in /api/routers should be manually added here. - */ -export const appRouter = createTRPCRouter({ - post: postRouter, -}); - -// export type definition of API -export type AppRouter = typeof appRouter; - -/** - * Create a server-side caller for the tRPC API. - * @example - * const trpc = createCaller(createContext); - * const res = await trpc.post.all(); - * ^? Post[] - */ -export const createCaller = createCallerFactory(appRouter); diff --git a/packages/cli/template/extras/src/server/api/routers/post/base.ts b/packages/cli/template/extras/src/server/api/routers/post/base.ts deleted file mode 100644 index 81684a39..00000000 --- a/packages/cli/template/extras/src/server/api/routers/post/base.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { z } from "zod/v4"; - -import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; - -// Mocked DB -interface Post { - id: number; - name: string; -} -const posts: Post[] = [ - { - id: 1, - name: "Hello World", - }, -]; - -export const postRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - - create: publicProcedure - .input(z.object({ name: z.string().min(1) })) - .mutation(async ({ input }) => { - const post: Post = { - id: posts.length + 1, - name: input.name, - }; - posts.push(post); - return post; - }), - - getLatest: publicProcedure.query(() => { - return posts.at(-1) ?? null; - }), -}); diff --git a/packages/cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts b/packages/cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts deleted file mode 100644 index 25d652c9..00000000 --- a/packages/cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { z } from "zod/v4"; - -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from "~/server/api/trpc"; -import { posts } from "~/server/db/schema"; - -export const postRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - - create: protectedProcedure - .input(z.object({ name: z.string().min(1) })) - .mutation(async ({ ctx, input }) => { - await ctx.db.insert(posts).values({ - name: input.name, - createdById: ctx.session.user.id, - }); - }), - - getLatest: publicProcedure.query(async ({ ctx }) => { - const post = await ctx.db.query.posts.findFirst({ - orderBy: (posts, { desc }) => [desc(posts.createdAt)], - }); - - return post ?? null; - }), - - getSecretMessage: protectedProcedure.query(() => { - return "you can now see this secret message!"; - }), -}); diff --git a/packages/cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts b/packages/cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts deleted file mode 100644 index af01f88e..00000000 --- a/packages/cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { z } from "zod/v4"; - -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from "~/server/api/trpc"; - -export const postRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - - create: protectedProcedure - .input(z.object({ name: z.string().min(1) })) - .mutation(async ({ ctx, input }) => { - return ctx.db.post.create({ - data: { - name: input.name, - createdBy: { connect: { id: ctx.session.user.id } }, - }, - }); - }), - - getLatest: protectedProcedure.query(async ({ ctx }) => { - const post = await ctx.db.post.findFirst({ - orderBy: { createdAt: "desc" }, - where: { createdBy: { id: ctx.session.user.id } }, - }); - - return post ?? null; - }), - - getSecretMessage: protectedProcedure.query(() => { - return "you can now see this secret message!"; - }), -}); diff --git a/packages/cli/template/extras/src/server/api/routers/post/with-auth.ts b/packages/cli/template/extras/src/server/api/routers/post/with-auth.ts deleted file mode 100644 index 55083180..00000000 --- a/packages/cli/template/extras/src/server/api/routers/post/with-auth.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { z } from "zod/v4"; - -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from "~/server/api/trpc"; - -let post = { - id: 1, - name: "Hello World", -}; - -export const postRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - - create: protectedProcedure - .input(z.object({ name: z.string().min(1) })) - .mutation(async ({ input }) => { - post = { id: post.id + 1, name: input.name }; - return post; - }), - - getLatest: protectedProcedure.query(() => { - return post; - }), - - getSecretMessage: protectedProcedure.query(() => { - return "you can now see this secret message!"; - }), -}); diff --git a/packages/cli/template/extras/src/server/api/routers/post/with-drizzle.ts b/packages/cli/template/extras/src/server/api/routers/post/with-drizzle.ts deleted file mode 100644 index 1c9bb7b7..00000000 --- a/packages/cli/template/extras/src/server/api/routers/post/with-drizzle.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { z } from "zod/v4"; - -import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; -import { posts } from "~/server/db/schema"; - -export const postRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - - create: publicProcedure - .input(z.object({ name: z.string().min(1) })) - .mutation(async ({ ctx, input }) => { - await ctx.db.insert(posts).values({ - name: input.name, - }); - }), - - getLatest: publicProcedure.query(async ({ ctx }) => { - const post = await ctx.db.query.posts.findFirst({ - orderBy: (posts, { desc }) => [desc(posts.createdAt)], - }); - - return post ?? null; - }), -}); diff --git a/packages/cli/template/extras/src/server/api/routers/post/with-prisma.ts b/packages/cli/template/extras/src/server/api/routers/post/with-prisma.ts deleted file mode 100644 index ad083cb1..00000000 --- a/packages/cli/template/extras/src/server/api/routers/post/with-prisma.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { z } from "zod/v4"; - -import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; - -export const postRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - - create: publicProcedure - .input(z.object({ name: z.string().min(1) })) - .mutation(async ({ ctx, input }) => { - return ctx.db.post.create({ - data: { - name: input.name, - }, - }); - }), - - getLatest: publicProcedure.query(async ({ ctx }) => { - const post = await ctx.db.post.findFirst({ - orderBy: { createdAt: "desc" }, - }); - - return post ?? null; - }), -}); diff --git a/packages/cli/template/extras/src/server/api/trpc-app/base.ts b/packages/cli/template/extras/src/server/api/trpc-app/base.ts deleted file mode 100644 index 9f8814e8..00000000 --- a/packages/cli/template/extras/src/server/api/trpc-app/base.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ -import { initTRPC } from "@trpc/server"; -import superjson from "superjson"; -import { ZodError } from "zod/v4"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - * - * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each - * wrap this and provides the required context. - * - * @see https://trpc.io/docs/server/context - */ -export const createTRPCContext = async (opts: { headers: Headers }) => { - return { - ...opts, - }; -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an articifial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/packages/cli/template/extras/src/server/api/trpc-app/with-auth-db.ts b/packages/cli/template/extras/src/server/api/trpc-app/with-auth-db.ts deleted file mode 100644 index 08d10c6b..00000000 --- a/packages/cli/template/extras/src/server/api/trpc-app/with-auth-db.ts +++ /dev/null @@ -1,133 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ - -import { initTRPC, TRPCError } from "@trpc/server"; -import superjson from "superjson"; -import { ZodError } from "zod/v4"; - -import { getServerAuthSession } from "~/server/auth"; -import { db } from "~/server/db"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - * - * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each - * wrap this and provides the required context. - * - * @see https://trpc.io/docs/server/context - */ -export const createTRPCContext = async (opts: { headers: Headers }) => { - const session = await getServerAuthSession(); - - return { - db, - session, - ...opts, - }; -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an articifial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); - -/** - * Protected (authenticated) procedure - * - * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies - * the session is valid and guarantees `ctx.session.user` is not null. - * - * @see https://trpc.io/docs/procedures - */ -export const protectedProcedure = t.procedure - .use(timingMiddleware) - .use(({ ctx, next }) => { - if (!ctx.session || !ctx.session.user) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - return next({ - ctx: { - // infers the `session` as non-nullable - session: { ...ctx.session, user: ctx.session.user }, - }, - }); - }); diff --git a/packages/cli/template/extras/src/server/api/trpc-app/with-auth.ts b/packages/cli/template/extras/src/server/api/trpc-app/with-auth.ts deleted file mode 100644 index 3510a908..00000000 --- a/packages/cli/template/extras/src/server/api/trpc-app/with-auth.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ -import { initTRPC, TRPCError } from "@trpc/server"; -import superjson from "superjson"; -import { ZodError } from "zod/v4"; - -import { getServerAuthSession } from "~/server/auth"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - * - * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each - * wrap this and provides the required context. - * - * @see https://trpc.io/docs/server/context - */ -export const createTRPCContext = async (opts: { headers: Headers }) => { - const session = await getServerAuthSession(); - - return { - session, - ...opts, - }; -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an articifial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); - -/** - * Protected (authenticated) procedure - * - * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies - * the session is valid and guarantees `ctx.session.user` is not null. - * - * @see https://trpc.io/docs/procedures - */ -export const protectedProcedure = t.procedure - .use(timingMiddleware) - .use(({ ctx, next }) => { - if (!ctx.session || !ctx.session.user) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - return next({ - ctx: { - // infers the `session` as non-nullable - session: { ...ctx.session, user: ctx.session.user }, - }, - }); - }); diff --git a/packages/cli/template/extras/src/server/api/trpc-app/with-db.ts b/packages/cli/template/extras/src/server/api/trpc-app/with-db.ts deleted file mode 100644 index b8349dc0..00000000 --- a/packages/cli/template/extras/src/server/api/trpc-app/with-db.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ -import { initTRPC } from "@trpc/server"; -import superjson from "superjson"; -import { ZodError } from "zod/v4"; - -import { db } from "~/server/db"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - * - * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each - * wrap this and provides the required context. - * - * @see https://trpc.io/docs/server/context - */ -export const createTRPCContext = async (opts: { headers: Headers }) => { - return { - db, - ...opts, - }; -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an articifial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/packages/cli/template/extras/src/server/api/trpc-pages/base.ts b/packages/cli/template/extras/src/server/api/trpc-pages/base.ts deleted file mode 100644 index 3fb05c12..00000000 --- a/packages/cli/template/extras/src/server/api/trpc-pages/base.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ - -import { initTRPC } from "@trpc/server"; -import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; -import superjson from "superjson"; -import { ZodError } from "zod/v4"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - */ - -type CreateContextOptions = Record; - -/** - * This helper generates the "internals" for a tRPC context. If you need to use it, you can export - * it from here. - * - * Examples of things you may need it for: - * - testing, so we don't have to mock Next.js' req/res - * - tRPC's `createSSGHelpers`, where we don't have req/res - * - * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts - */ -const createInnerTRPCContext = (_opts: CreateContextOptions) => { - return {}; -}; - -/** - * This is the actual context you will use in your router. It will be used to process every request - * that goes through your tRPC endpoint. - * - * @see https://trpc.io/docs/context - */ -export const createTRPCContext = (_opts: CreateNextContextOptions) => { - return createInnerTRPCContext({}); -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ - -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an articifial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/packages/cli/template/extras/src/server/api/trpc-pages/with-auth-db.ts b/packages/cli/template/extras/src/server/api/trpc-pages/with-auth-db.ts deleted file mode 100644 index f7dc0e5d..00000000 --- a/packages/cli/template/extras/src/server/api/trpc-pages/with-auth-db.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ - -import { initTRPC, TRPCError } from "@trpc/server"; -import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; -import type { Session } from "next-auth"; -import superjson from "superjson"; -import { ZodError } from "zod/v4"; - -import { getServerAuthSession } from "~/server/auth"; -import { db } from "~/server/db"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - */ - -interface CreateContextOptions { - session: Session | null; -} - -/** - * This helper generates the "internals" for a tRPC context. If you need to use it, you can export - * it from here. - * - * Examples of things you may need it for: - * - testing, so we don't have to mock Next.js' req/res - * - tRPC's `createSSGHelpers`, where we don't have req/res - * - * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts - */ -const createInnerTRPCContext = (opts: CreateContextOptions) => { - return { - session: opts.session, - db, - }; -}; - -/** - * This is the actual context you will use in your router. It will be used to process every request - * that goes through your tRPC endpoint. - * - * @see https://trpc.io/docs/context - */ -export const createTRPCContext = async (opts: CreateNextContextOptions) => { - const { req, res } = opts; - - // Get the session from the server using the getServerSession wrapper function - const session = await getServerAuthSession({ req, res }); - - return createInnerTRPCContext({ - session, - }); -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ - -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an articifial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); - -/** - * Protected (authenticated) procedure - * - * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies - * the session is valid and guarantees `ctx.session.user` is not null. - * - * @see https://trpc.io/docs/procedures - */ -export const protectedProcedure = t.procedure - .use(timingMiddleware) - .use(({ ctx, next }) => { - if (!ctx.session || !ctx.session.user) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - return next({ - ctx: { - // infers the `session` as non-nullable - session: { ...ctx.session, user: ctx.session.user }, - }, - }); - }); diff --git a/packages/cli/template/extras/src/server/api/trpc-pages/with-auth.ts b/packages/cli/template/extras/src/server/api/trpc-pages/with-auth.ts deleted file mode 100644 index 4d795bb3..00000000 --- a/packages/cli/template/extras/src/server/api/trpc-pages/with-auth.ts +++ /dev/null @@ -1,158 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ -import { initTRPC, TRPCError } from "@trpc/server"; -import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; -import type { Session } from "next-auth"; -import superjson from "superjson"; -import { ZodError } from "zod/v4"; - -import { getServerAuthSession } from "~/server/auth"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - */ - -interface CreateContextOptions { - session: Session | null; -} - -/** - * This helper generates the "internals" for a tRPC context. If you need to use it, you can export - * it from here. - * - * Examples of things you may need it for: - * - testing, so we don't have to mock Next.js' req/res - * - tRPC's `createSSGHelpers`, where we don't have req/res - * - * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts - */ -const createInnerTRPCContext = ({ session }: CreateContextOptions) => { - return { - session, - }; -}; - -/** - * This is the actual context you will use in your router. It will be used to process every request - * that goes through your tRPC endpoint. - * - * @see https://trpc.io/docs/context - */ -export const createTRPCContext = async ({ - req, - res, -}: CreateNextContextOptions) => { - // Get the session from the server using the getServerSession wrapper function - const session = await getServerAuthSession({ req, res }); - - return createInnerTRPCContext({ - session, - }); -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ - -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an articifial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); - -/** - * Protected (authenticated) procedure - * - * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies - * the session is valid and guarantees `ctx.session.user` is not null. - * - * @see https://trpc.io/docs/procedures - */ -export const protectedProcedure = t.procedure - .use(timingMiddleware) - .use(({ ctx, next }) => { - if (!ctx.session || !ctx.session.user) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - return next({ - ctx: { - // infers the `session` as non-nullable - session: { ...ctx.session, user: ctx.session.user }, - }, - }); - }); diff --git a/packages/cli/template/extras/src/server/api/trpc-pages/with-db.ts b/packages/cli/template/extras/src/server/api/trpc-pages/with-db.ts deleted file mode 100644 index cf0be485..00000000 --- a/packages/cli/template/extras/src/server/api/trpc-pages/with-db.ts +++ /dev/null @@ -1,125 +0,0 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ -import { initTRPC } from "@trpc/server"; -import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; -import superjson from "superjson"; -import { ZodError } from "zod/v4"; - -import { db } from "~/server/db"; - -/** - * 1. CONTEXT - * - * This section defines the "contexts" that are available in the backend API. - * - * These allow you to access things when processing a request, like the database, the session, etc. - */ - -type CreateContextOptions = Record; - -/** - * This helper generates the "internals" for a tRPC context. If you need to use it, you can export - * it from here. - * - * Examples of things you may need it for: - * - testing, so we don't have to mock Next.js' req/res - * - tRPC's `createSSGHelpers`, where we don't have req/res - * - * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts - */ -const createInnerTRPCContext = (_opts: CreateContextOptions) => { - return { - db, - }; -}; - -/** - * This is the actual context you will use in your router. It will be used to process every request - * that goes through your tRPC endpoint. - * - * @see https://trpc.io/docs/context - */ -export const createTRPCContext = (_opts: CreateNextContextOptions) => { - return createInnerTRPCContext({}); -}; - -/** - * 2. INITIALIZATION - * - * This is where the tRPC API is initialized, connecting the context and transformer. We also parse - * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation - * errors on the backend. - */ - -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); - -/** - * Create a server-side caller. - * - * @see https://trpc.io/docs/server/server-side-calls - */ -export const createCallerFactory = t.createCallerFactory; - -/** - * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) - * - * These are the pieces you use to build your tRPC API. You should import these a lot in the - * "/src/server/api/routers" directory. - */ - -/** - * This is how you create new routers and sub-routers in your tRPC API. - * - * @see https://trpc.io/docs/router - */ -export const createTRPCRouter = t.router; - -/** - * Middleware for timing procedure execution and adding an articifial delay in development. - * - * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating - * network latency that would occur in production but not in local development. - */ -const timingMiddleware = t.middleware(async ({ next, path }) => { - const start = Date.now(); - - if (t._config.isDev) { - // artificial delay in dev - const waitMs = Math.floor(Math.random() * 400) + 100; - await new Promise((resolve) => setTimeout(resolve, waitMs)); - } - - const result = await next(); - - const end = Date.now(); - console.log(`[TRPC] ${path} took ${end - start}ms to execute`); - - return result; -}); - -/** - * Public (unauthenticated) procedure - * - * This is the base piece you use to build new queries and mutations on your tRPC API. It does not - * guarantee that a user querying is authorized, but you can still access user session data if they - * are logged in. - */ -export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/packages/cli/template/extras/src/server/data/users.ts b/packages/cli/template/extras/src/server/data/users.ts deleted file mode 100644 index c5074992..00000000 --- a/packages/cli/template/extras/src/server/data/users.ts +++ /dev/null @@ -1,23 +0,0 @@ -import "server-only"; - -import { fmAdapter } from "../auth"; -import { saltAndHashPassword } from "../password"; - -type UserSignUpInput = { - email: string; - password: string; -}; - -export async function userSignUp(input: UserSignUpInput) { - const passwordHash = await saltAndHashPassword(input.password); - - // create the user in our database - const user = await fmAdapter.typedClients.userWithPasswordHash.create({ - fieldData: { - email: input.email, - passwordHash, - }, - }); - - return user; -} diff --git a/packages/cli/template/extras/src/server/db/db-prisma-planetscale.ts b/packages/cli/template/extras/src/server/db/db-prisma-planetscale.ts deleted file mode 100644 index fc54a43a..00000000 --- a/packages/cli/template/extras/src/server/db/db-prisma-planetscale.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Client } from "@planetscale/database"; -import { PrismaPlanetScale } from "@prisma/adapter-planetscale"; -import { PrismaClient } from "@prisma/client"; - -import { env } from "~/env"; - -const psClient = new Client({ url: env.DATABASE_URL }); - -const createPrismaClient = () => - new PrismaClient({ - log: - env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], - adapter: new PrismaPlanetScale(psClient), - }); - -const globalForPrisma = globalThis as unknown as { - prisma: ReturnType | undefined; -}; - -export const db = globalForPrisma.prisma ?? createPrismaClient(); - -if (env.NODE_ENV !== "production") globalForPrisma.prisma = db; diff --git a/packages/cli/template/extras/src/server/db/db-prisma.ts b/packages/cli/template/extras/src/server/db/db-prisma.ts deleted file mode 100644 index 8b2507b7..00000000 --- a/packages/cli/template/extras/src/server/db/db-prisma.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { PrismaClient } from "@prisma/client"; - -import { env } from "~/env"; - -const createPrismaClient = () => - new PrismaClient({ - log: - env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], - }); - -const globalForPrisma = globalThis as unknown as { - prisma: ReturnType | undefined; -}; - -export const db = globalForPrisma.prisma ?? createPrismaClient(); - -if (env.NODE_ENV !== "production") globalForPrisma.prisma = db; diff --git a/packages/cli/template/extras/src/server/db/index-drizzle/with-mysql.ts b/packages/cli/template/extras/src/server/db/index-drizzle/with-mysql.ts deleted file mode 100644 index 807acea9..00000000 --- a/packages/cli/template/extras/src/server/db/index-drizzle/with-mysql.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { drizzle } from "drizzle-orm/mysql2"; -import { createPool, type Pool } from "mysql2/promise"; - -import { env } from "~/env"; -import * as schema from "./schema"; - -/** - * Cache the database connection in development. This avoids creating a new connection on every HMR - * update. - */ -const globalForDb = globalThis as unknown as { - conn: Pool | undefined; -}; - -const conn = globalForDb.conn ?? createPool({ uri: env.DATABASE_URL }); -if (env.NODE_ENV !== "production") globalForDb.conn = conn; - -export const db = drizzle(conn, { schema, mode: "default" }); diff --git a/packages/cli/template/extras/src/server/db/index-drizzle/with-planetscale.ts b/packages/cli/template/extras/src/server/db/index-drizzle/with-planetscale.ts deleted file mode 100644 index 4613a4c1..00000000 --- a/packages/cli/template/extras/src/server/db/index-drizzle/with-planetscale.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Client } from "@planetscale/database"; -import { drizzle } from "drizzle-orm/planetscale-serverless"; - -import { env } from "~/env"; -import * as schema from "./schema"; - -export const db = drizzle(new Client({ url: env.DATABASE_URL }), { schema }); diff --git a/packages/cli/template/extras/src/server/db/index-drizzle/with-postgres.ts b/packages/cli/template/extras/src/server/db/index-drizzle/with-postgres.ts deleted file mode 100644 index df18baff..00000000 --- a/packages/cli/template/extras/src/server/db/index-drizzle/with-postgres.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { drizzle } from "drizzle-orm/postgres-js"; -import postgres from "postgres"; - -import { env } from "~/env"; -import * as schema from "./schema"; - -/** - * Cache the database connection in development. This avoids creating a new connection on every HMR - * update. - */ -const globalForDb = globalThis as unknown as { - conn: postgres.Sql | undefined; -}; - -const conn = globalForDb.conn ?? postgres(env.DATABASE_URL); -if (env.NODE_ENV !== "production") globalForDb.conn = conn; - -export const db = drizzle(conn, { schema }); diff --git a/packages/cli/template/extras/src/server/db/index-drizzle/with-sqlite.ts b/packages/cli/template/extras/src/server/db/index-drizzle/with-sqlite.ts deleted file mode 100644 index 274f7413..00000000 --- a/packages/cli/template/extras/src/server/db/index-drizzle/with-sqlite.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { type Client, createClient } from "@libsql/client"; -import { drizzle } from "drizzle-orm/libsql"; - -import { env } from "~/env"; -import * as schema from "./schema"; - -/** - * Cache the database connection in development. This avoids creating a new connection on every HMR - * update. - */ -const globalForDb = globalThis as unknown as { - client: Client | undefined; -}; - -export const client = - globalForDb.client ?? createClient({ url: env.DATABASE_URL }); -if (env.NODE_ENV !== "production") globalForDb.client = client; - -export const db = drizzle(client, { schema }); diff --git a/packages/cli/template/extras/src/server/db/schema-drizzle/base-mysql.ts b/packages/cli/template/extras/src/server/db/schema-drizzle/base-mysql.ts deleted file mode 100644 index 79b03dc3..00000000 --- a/packages/cli/template/extras/src/server/db/schema-drizzle/base-mysql.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Example model schema from the Drizzle docs -// https://orm.drizzle.team/docs/sql-schema-declaration - -import { sql } from "drizzle-orm"; -import { - bigint, - index, - mysqlTableCreator, - timestamp, - varchar, -} from "drizzle-orm/mysql-core"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = mysqlTableCreator((name) => `project1_${name}`); - -export const posts = createTable( - "post", - { - id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), - name: varchar("name", { length: 256 }), - createdAt: timestamp("created_at") - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), - }, - (example) => ({ - nameIndex: index("name_idx").on(example.name), - }), -); diff --git a/packages/cli/template/extras/src/server/db/schema-drizzle/base-planetscale.ts b/packages/cli/template/extras/src/server/db/schema-drizzle/base-planetscale.ts deleted file mode 100644 index 79b03dc3..00000000 --- a/packages/cli/template/extras/src/server/db/schema-drizzle/base-planetscale.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Example model schema from the Drizzle docs -// https://orm.drizzle.team/docs/sql-schema-declaration - -import { sql } from "drizzle-orm"; -import { - bigint, - index, - mysqlTableCreator, - timestamp, - varchar, -} from "drizzle-orm/mysql-core"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = mysqlTableCreator((name) => `project1_${name}`); - -export const posts = createTable( - "post", - { - id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), - name: varchar("name", { length: 256 }), - createdAt: timestamp("created_at") - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), - }, - (example) => ({ - nameIndex: index("name_idx").on(example.name), - }), -); diff --git a/packages/cli/template/extras/src/server/db/schema-drizzle/base-postgres.ts b/packages/cli/template/extras/src/server/db/schema-drizzle/base-postgres.ts deleted file mode 100644 index 6433f25d..00000000 --- a/packages/cli/template/extras/src/server/db/schema-drizzle/base-postgres.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Example model schema from the Drizzle docs -// https://orm.drizzle.team/docs/sql-schema-declaration - -import { sql } from "drizzle-orm"; -import { - index, - pgTableCreator, - serial, - timestamp, - varchar, -} from "drizzle-orm/pg-core"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = pgTableCreator((name) => `project1_${name}`); - -export const posts = createTable( - "post", - { - id: serial("id").primaryKey(), - name: varchar("name", { length: 256 }), - createdAt: timestamp("created_at", { withTimezone: true }) - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( - () => new Date(), - ), - }, - (example) => ({ - nameIndex: index("name_idx").on(example.name), - }), -); diff --git a/packages/cli/template/extras/src/server/db/schema-drizzle/base-sqlite.ts b/packages/cli/template/extras/src/server/db/schema-drizzle/base-sqlite.ts deleted file mode 100644 index 792fe71f..00000000 --- a/packages/cli/template/extras/src/server/db/schema-drizzle/base-sqlite.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Example model schema from the Drizzle docs -// https://orm.drizzle.team/docs/sql-schema-declaration - -import { sql } from "drizzle-orm"; -import { index, int, sqliteTableCreator, text } from "drizzle-orm/sqlite-core"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = sqliteTableCreator((name) => `project1_${name}`); - -export const posts = createTable( - "post", - { - id: int("id", { mode: "number" }).primaryKey({ autoIncrement: true }), - name: text("name", { length: 256 }), - createdAt: int("created_at", { mode: "timestamp" }) - .default(sql`(unixepoch())`) - .notNull(), - updatedAt: int("updated_at", { mode: "timestamp" }).$onUpdate( - () => new Date(), - ), - }, - (example) => ({ - nameIndex: index("name_idx").on(example.name), - }), -); diff --git a/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts b/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts deleted file mode 100644 index 63781a66..00000000 --- a/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { relations, sql } from "drizzle-orm"; -import { - bigint, - index, - int, - mysqlTableCreator, - primaryKey, - text, - timestamp, - varchar, -} from "drizzle-orm/mysql-core"; -import type { AdapterAccount } from "next-auth/adapters"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = mysqlTableCreator((name) => `project1_${name}`); - -export const posts = createTable( - "post", - { - id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), - name: varchar("name", { length: 256 }), - createdById: varchar("created_by", { length: 255 }) - .notNull() - .references(() => users.id), - createdAt: timestamp("created_at") - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), - }, - (example) => ({ - createdByIdIdx: index("created_by_idx").on(example.createdById), - nameIndex: index("name_idx").on(example.name), - }), -); - -export const users = createTable("user", { - id: varchar("id", { length: 255 }) - .notNull() - .primaryKey() - .$defaultFn(() => crypto.randomUUID()), - name: varchar("name", { length: 255 }), - email: varchar("email", { length: 255 }).notNull(), - emailVerified: timestamp("email_verified", { - mode: "date", - fsp: 3, - }).default(sql`CURRENT_TIMESTAMP(3)`), - image: varchar("image", { length: 255 }), -}); - -export const usersRelations = relations(users, ({ many }) => ({ - accounts: many(accounts), - sessions: many(sessions), -})); - -export const accounts = createTable( - "account", - { - userId: varchar("user_id", { length: 255 }) - .notNull() - .references(() => users.id), - type: varchar("type", { length: 255 }) - .$type() - .notNull(), - provider: varchar("provider", { length: 255 }).notNull(), - providerAccountId: varchar("provider_account_id", { - length: 255, - }).notNull(), - refresh_token: text("refresh_token"), - access_token: text("access_token"), - expires_at: int("expires_at"), - token_type: varchar("token_type", { length: 255 }), - scope: varchar("scope", { length: 255 }), - id_token: text("id_token"), - session_state: varchar("session_state", { length: 255 }), - }, - (account) => ({ - compoundKey: primaryKey({ - columns: [account.provider, account.providerAccountId], - }), - userIdIdx: index("account_user_id_idx").on(account.userId), - }), -); - -export const accountsRelations = relations(accounts, ({ one }) => ({ - user: one(users, { fields: [accounts.userId], references: [users.id] }), -})); - -export const sessions = createTable( - "session", - { - sessionToken: varchar("session_token", { length: 255 }) - .notNull() - .primaryKey(), - userId: varchar("user_id", { length: 255 }) - .notNull() - .references(() => users.id), - expires: timestamp("expires", { mode: "date" }).notNull(), - }, - (session) => ({ - userIdIdx: index("session_user_id_idx").on(session.userId), - }), -); - -export const sessionsRelations = relations(sessions, ({ one }) => ({ - user: one(users, { fields: [sessions.userId], references: [users.id] }), -})); - -export const verificationTokens = createTable( - "verification_token", - { - identifier: varchar("identifier", { length: 255 }).notNull(), - token: varchar("token", { length: 255 }).notNull(), - expires: timestamp("expires", { mode: "date" }).notNull(), - }, - (vt) => ({ - compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), - }), -); diff --git a/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts b/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts deleted file mode 100644 index 601e5e38..00000000 --- a/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { relations, sql } from "drizzle-orm"; -import { - bigint, - index, - int, - mysqlTableCreator, - primaryKey, - text, - timestamp, - varchar, -} from "drizzle-orm/mysql-core"; -import type { AdapterAccount } from "next-auth/adapters"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = mysqlTableCreator((name) => `project1_${name}`); - -export const posts = createTable( - "post", - { - id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), - name: varchar("name", { length: 256 }), - createdById: varchar("created_by", { length: 255 }).notNull(), - createdAt: timestamp("created_at") - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp("updated_at").onUpdateNow(), - }, - (example) => ({ - createdByIdIdx: index("created_by_idx").on(example.createdById), - nameIndex: index("name_idx").on(example.name), - }), -); - -export const users = createTable("user", { - id: varchar("id", { length: 255 }) - .notNull() - .primaryKey() - .$defaultFn(() => crypto.randomUUID()), - name: varchar("name", { length: 255 }), - email: varchar("email", { length: 255 }).notNull(), - emailVerified: timestamp("email_verified", { - mode: "date", - fsp: 3, - }).default(sql`CURRENT_TIMESTAMP(3)`), - image: varchar("image", { length: 255 }), -}); - -export const usersRelations = relations(users, ({ many }) => ({ - accounts: many(accounts), - sessions: many(sessions), -})); - -export const accounts = createTable( - "account", - { - userId: varchar("user_id", { length: 255 }).notNull(), - type: varchar("type", { length: 255 }) - .$type() - .notNull(), - provider: varchar("provider", { length: 255 }).notNull(), - providerAccountId: varchar("provider_account_id", { - length: 255, - }).notNull(), - refresh_token: text("refresh_token"), - access_token: text("access_token"), - expires_at: int("expires_at"), - token_type: varchar("token_type", { length: 255 }), - scope: varchar("scope", { length: 255 }), - id_token: text("id_token"), - session_state: varchar("session_state", { length: 255 }), - }, - (account) => ({ - compoundKey: primaryKey({ - columns: [account.provider, account.providerAccountId], - }), - userIdIdx: index("accounts_user_id_idx").on(account.userId), - }), -); - -export const accountsRelations = relations(accounts, ({ one }) => ({ - user: one(users, { fields: [accounts.userId], references: [users.id] }), -})); - -export const sessions = createTable( - "session", - { - sessionToken: varchar("session_token", { length: 255 }) - .notNull() - .primaryKey(), - userId: varchar("user_id", { length: 255 }).notNull(), - expires: timestamp("expires", { mode: "date" }).notNull(), - }, - (session) => ({ - userIdIdx: index("session_user_id_idx").on(session.userId), - }), -); - -export const sessionsRelations = relations(sessions, ({ one }) => ({ - user: one(users, { fields: [sessions.userId], references: [users.id] }), -})); - -export const verificationTokens = createTable( - "verification_token", - { - identifier: varchar("identifier", { length: 255 }).notNull(), - token: varchar("token", { length: 255 }).notNull(), - expires: timestamp("expires", { mode: "date" }).notNull(), - }, - (vt) => ({ - compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), - }), -); diff --git a/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts b/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts deleted file mode 100644 index 810e13ab..00000000 --- a/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { relations, sql } from "drizzle-orm"; -import { - index, - integer, - pgTableCreator, - primaryKey, - serial, - text, - timestamp, - varchar, -} from "drizzle-orm/pg-core"; -import type { AdapterAccount } from "next-auth/adapters"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = pgTableCreator((name) => `project1_${name}`); - -export const posts = createTable( - "post", - { - id: serial("id").primaryKey(), - name: varchar("name", { length: 256 }), - createdById: varchar("created_by", { length: 255 }) - .notNull() - .references(() => users.id), - createdAt: timestamp("created_at", { withTimezone: true }) - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( - () => new Date(), - ), - }, - (example) => ({ - createdByIdIdx: index("created_by_idx").on(example.createdById), - nameIndex: index("name_idx").on(example.name), - }), -); - -export const users = createTable("user", { - id: varchar("id", { length: 255 }) - .notNull() - .primaryKey() - .$defaultFn(() => crypto.randomUUID()), - name: varchar("name", { length: 255 }), - email: varchar("email", { length: 255 }).notNull(), - emailVerified: timestamp("email_verified", { - mode: "date", - withTimezone: true, - }).default(sql`CURRENT_TIMESTAMP`), - image: varchar("image", { length: 255 }), -}); - -export const usersRelations = relations(users, ({ many }) => ({ - accounts: many(accounts), -})); - -export const accounts = createTable( - "account", - { - userId: varchar("user_id", { length: 255 }) - .notNull() - .references(() => users.id), - type: varchar("type", { length: 255 }) - .$type() - .notNull(), - provider: varchar("provider", { length: 255 }).notNull(), - providerAccountId: varchar("provider_account_id", { - length: 255, - }).notNull(), - refresh_token: text("refresh_token"), - access_token: text("access_token"), - expires_at: integer("expires_at"), - token_type: varchar("token_type", { length: 255 }), - scope: varchar("scope", { length: 255 }), - id_token: text("id_token"), - session_state: varchar("session_state", { length: 255 }), - }, - (account) => ({ - compoundKey: primaryKey({ - columns: [account.provider, account.providerAccountId], - }), - userIdIdx: index("account_user_id_idx").on(account.userId), - }), -); - -export const accountsRelations = relations(accounts, ({ one }) => ({ - user: one(users, { fields: [accounts.userId], references: [users.id] }), -})); - -export const sessions = createTable( - "session", - { - sessionToken: varchar("session_token", { length: 255 }) - .notNull() - .primaryKey(), - userId: varchar("user_id", { length: 255 }) - .notNull() - .references(() => users.id), - expires: timestamp("expires", { - mode: "date", - withTimezone: true, - }).notNull(), - }, - (session) => ({ - userIdIdx: index("session_user_id_idx").on(session.userId), - }), -); - -export const sessionsRelations = relations(sessions, ({ one }) => ({ - user: one(users, { fields: [sessions.userId], references: [users.id] }), -})); - -export const verificationTokens = createTable( - "verification_token", - { - identifier: varchar("identifier", { length: 255 }).notNull(), - token: varchar("token", { length: 255 }).notNull(), - expires: timestamp("expires", { - mode: "date", - withTimezone: true, - }).notNull(), - }, - (vt) => ({ - compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), - }), -); diff --git a/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts b/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts deleted file mode 100644 index 3af6c96f..00000000 --- a/packages/cli/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { relations, sql } from "drizzle-orm"; -import { - index, - int, - primaryKey, - sqliteTableCreator, - text, -} from "drizzle-orm/sqlite-core"; -import type { AdapterAccount } from "next-auth/adapters"; - -/** - * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same - * database instance for multiple projects. - * - * @see https://orm.drizzle.team/docs/goodies#multi-project-schema - */ -export const createTable = sqliteTableCreator((name) => `project1_${name}`); - -export const posts = createTable( - "post", - { - id: int("id", { mode: "number" }).primaryKey({ autoIncrement: true }), - name: text("name", { length: 256 }), - createdById: text("created_by", { length: 255 }) - .notNull() - .references(() => users.id), - createdAt: int("created_at", { mode: "timestamp" }) - .default(sql`(unixepoch())`) - .notNull(), - updatedAt: int("updatedAt", { mode: "timestamp" }).$onUpdate( - () => new Date(), - ), - }, - (example) => ({ - createdByIdIdx: index("created_by_idx").on(example.createdById), - nameIndex: index("name_idx").on(example.name), - }), -); - -export const users = createTable("user", { - id: text("id", { length: 255 }) - .notNull() - .primaryKey() - .$defaultFn(() => crypto.randomUUID()), - name: text("name", { length: 255 }), - email: text("email", { length: 255 }).notNull(), - emailVerified: int("email_verified", { - mode: "timestamp", - }).default(sql`(unixepoch())`), - image: text("image", { length: 255 }), -}); - -export const usersRelations = relations(users, ({ many }) => ({ - accounts: many(accounts), -})); - -export const accounts = createTable( - "account", - { - userId: text("user_id", { length: 255 }) - .notNull() - .references(() => users.id), - type: text("type", { length: 255 }) - .$type() - .notNull(), - provider: text("provider", { length: 255 }).notNull(), - providerAccountId: text("provider_account_id", { length: 255 }).notNull(), - refresh_token: text("refresh_token"), - access_token: text("access_token"), - expires_at: int("expires_at"), - token_type: text("token_type", { length: 255 }), - scope: text("scope", { length: 255 }), - id_token: text("id_token"), - session_state: text("session_state", { length: 255 }), - }, - (account) => ({ - compoundKey: primaryKey({ - columns: [account.provider, account.providerAccountId], - }), - userIdIdx: index("account_user_id_idx").on(account.userId), - }), -); - -export const accountsRelations = relations(accounts, ({ one }) => ({ - user: one(users, { fields: [accounts.userId], references: [users.id] }), -})); - -export const sessions = createTable( - "session", - { - sessionToken: text("session_token", { length: 255 }).notNull().primaryKey(), - userId: text("userId", { length: 255 }) - .notNull() - .references(() => users.id), - expires: int("expires", { mode: "timestamp" }).notNull(), - }, - (session) => ({ - userIdIdx: index("session_userId_idx").on(session.userId), - }), -); - -export const sessionsRelations = relations(sessions, ({ one }) => ({ - user: one(users, { fields: [sessions.userId], references: [users.id] }), -})); - -export const verificationTokens = createTable( - "verification_token", - { - identifier: text("identifier", { length: 255 }).notNull(), - token: text("token", { length: 255 }).notNull(), - expires: int("expires", { mode: "timestamp" }).notNull(), - }, - (vt) => ({ - compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), - }), -); diff --git a/packages/cli/template/extras/src/server/next-auth/base.ts b/packages/cli/template/extras/src/server/next-auth/base.ts deleted file mode 100644 index 2f8cde96..00000000 --- a/packages/cli/template/extras/src/server/next-auth/base.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { OttoAdapter } from "@proofkit/fmdapi"; -import NextAuth, { type DefaultSession } from "next-auth"; -import type { Provider } from "next-auth/providers"; -import Credentials from "next-auth/providers/credentials"; -import { FilemakerAdapter } from "next-auth-adapter-filemaker"; -import { z } from "zod/v4"; -import { env } from "@/config/env"; - -import { verifyPassword } from "./password"; - -export const fmAdapter = FilemakerAdapter({ - adapter: new OttoAdapter({ - auth: { apiKey: env.OTTO_API_KEY }, - db: env.FM_DATABASE, - server: env.FM_SERVER, - }), -}); - -/** - * Module augmentation for `next-auth` types. Alldows us to add custom properties to the `session` - * object and keep type safety. - * - * @see https://next-auth.js.org/getting-started/typescript#module-augmentation - */ -declare module "next-auth" { - interface Session extends DefaultSession { - user: { - id: string; - // ...other properties - // role: UserRole; - } & DefaultSession["user"]; - } - - // interface User { - // // ...other properties - // // role: UserRole; - // } -} - -const signInSchema = z.object({ - email: z.string().email(), - password: z.string(), -}); - -const providers: Provider[] = [ - Credentials({ - credentials: { - email: { label: "Email", type: "email" }, - password: { label: "Password", type: "password" }, - }, - authorize: async (credentials) => { - const parsed = signInSchema.safeParse(credentials); - if (!parsed.success) { - return null; - } - - const { email, password } = parsed.data; - - try { - // logic to verify if the user exists with the password hash - const userResponse = - await fmAdapter.typedClients.userWithPasswordHash.findOne({ - query: { email: `==${email.replace("@", "\\@")}` }, - }); - const { passwordHash, ...userData } = userResponse.data.fieldData; - const isValid = await verifyPassword(password, passwordHash); - if (!isValid) return null; - - return userData; - } catch (error) { - console.log("error", error); - throw new Error("User not found."); - } - }, - }), -]; - -export const providerMap = providers - .map((provider) => { - if (typeof provider === "function") { - const providerData = provider(); - return { id: providerData.id, name: providerData.name }; - } else { - return { id: provider.id, name: provider.name }; - } - }) - .filter((provider) => provider.id !== "credentials"); - -export const { auth, handlers, signIn, signOut } = NextAuth({ - pages: { - signIn: "/auth/signin", - newUser: "/auth/signup", - error: "/auth/signin", - }, - callbacks: { - session: ({ session, token }) => ({ - ...session, - user: { - ...session.user, - id: token.sub, - }, - }), - }, - adapter: fmAdapter.Adapter, - session: { strategy: "jwt" }, - providers, -}); diff --git a/packages/cli/template/extras/src/server/next-auth/password.ts b/packages/cli/template/extras/src/server/next-auth/password.ts deleted file mode 100644 index d0cd6a95..00000000 --- a/packages/cli/template/extras/src/server/next-auth/password.ts +++ /dev/null @@ -1,13 +0,0 @@ -export async function saltAndHashPassword(password: string): Promise { - const bcrypt = await import("bcrypt"); - const saltRounds = 12; - return bcrypt.hash(password, saltRounds); -} - -export async function verifyPassword( - plainTextPassword: string, - hashedPassword: string, -): Promise { - const bcrypt = await import("bcrypt"); - return bcrypt.compare(plainTextPassword, hashedPassword); -} diff --git a/packages/cli/template/extras/src/server/next-auth/with-drizzle.ts b/packages/cli/template/extras/src/server/next-auth/with-drizzle.ts deleted file mode 100644 index 63879d2e..00000000 --- a/packages/cli/template/extras/src/server/next-auth/with-drizzle.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { DrizzleAdapter } from "@auth/drizzle-adapter"; -import { - type DefaultSession, - getServerSession, - type NextAuthOptions, -} from "next-auth"; -import type { Adapter } from "next-auth/adapters"; -import DiscordProvider from "next-auth/providers/discord"; - -import { env } from "~/env"; -import { db } from "~/server/db"; -import { - accounts, - sessions, - users, - verificationTokens, -} from "~/server/db/schema"; - -/** - * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` - * object and keep type safety. - * - * @see https://next-auth.js.org/getting-started/typescript#module-augmentation - */ -declare module "next-auth" { - interface Session extends DefaultSession { - user: { - id: string; - // ...other properties - // role: UserRole; - } & DefaultSession["user"]; - } - - // interface User { - // // ...other properties - // // role: UserRole; - // } -} - -/** - * Options for NextAuth.js used to configure adapters, providers, callbacks, etc. - * - * @see https://next-auth.js.org/configuration/options - */ -export const authOptions: NextAuthOptions = { - callbacks: { - session: ({ session, user }) => ({ - ...session, - user: { - ...session.user, - id: user.id, - }, - }), - }, - adapter: DrizzleAdapter(db, { - usersTable: users, - accountsTable: accounts, - sessionsTable: sessions, - verificationTokensTable: verificationTokens, - }) as Adapter, - providers: [ - DiscordProvider({ - clientId: env.DISCORD_CLIENT_ID, - clientSecret: env.DISCORD_CLIENT_SECRET, - }), - /** - * ...add more providers here. - * - * Most other providers require a bit more work than the Discord provider. For example, the - * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account - * model. Refer to the NextAuth.js docs for the provider you want to use. Example: - * - * @see https://next-auth.js.org/providers/github - */ - ], -}; - -/** - * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file. - * - * @see https://next-auth.js.org/configuration/nextjs - */ -export const getServerAuthSession = () => getServerSession(authOptions); diff --git a/packages/cli/template/extras/src/server/next-auth/with-prisma.ts b/packages/cli/template/extras/src/server/next-auth/with-prisma.ts deleted file mode 100644 index 2fb4c0ec..00000000 --- a/packages/cli/template/extras/src/server/next-auth/with-prisma.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { PrismaAdapter } from "@auth/prisma-adapter"; -import { - type DefaultSession, - getServerSession, - type NextAuthOptions, -} from "next-auth"; -import type { Adapter } from "next-auth/adapters"; -import DiscordProvider from "next-auth/providers/discord"; - -import { env } from "~/env"; -import { db } from "~/server/db"; - -/** - * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` - * object and keep type safety. - * - * @see https://next-auth.js.org/getting-started/typescript#module-augmentation - */ -declare module "next-auth" { - interface Session extends DefaultSession { - user: { - id: string; - // ...other properties - // role: UserRole; - } & DefaultSession["user"]; - } - - // interface User { - // // ...other properties - // // role: UserRole; - // } -} - -/** - * Options for NextAuth.js used to configure adapters, providers, callbacks, etc. - * - * @see https://next-auth.js.org/configuration/options - */ -export const authOptions: NextAuthOptions = { - callbacks: { - session: ({ session, user }) => ({ - ...session, - user: { - ...session.user, - id: user.id, - }, - }), - }, - adapter: PrismaAdapter(db) as Adapter, - providers: [ - DiscordProvider({ - clientId: env.DISCORD_CLIENT_ID, - clientSecret: env.DISCORD_CLIENT_SECRET, - }), - /** - * ...add more providers here. - * - * Most other providers require a bit more work than the Discord provider. For example, the - * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account - * model. Refer to the NextAuth.js docs for the provider you want to use. Example: - * - * @see https://next-auth.js.org/providers/github - */ - ], -}; - -/** - * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file. - * - * @see https://next-auth.js.org/configuration/nextjs - */ -export const getServerAuthSession = () => getServerSession(authOptions); diff --git a/packages/cli/template/extras/src/trpc/query-client.ts b/packages/cli/template/extras/src/trpc/query-client.ts deleted file mode 100644 index b89a1794..00000000 --- a/packages/cli/template/extras/src/trpc/query-client.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - defaultShouldDehydrateQuery, - QueryClient, -} from "@tanstack/react-query"; -import SuperJSON from "superjson"; - -export const createQueryClient = () => - new QueryClient({ - defaultOptions: { - queries: { - // With SSR, we usually want to set some default staleTime - // above 0 to avoid refetching immediately on the client - staleTime: 30 * 1000, - }, - dehydrate: { - serializeData: SuperJSON.serialize, - shouldDehydrateQuery: (query) => - defaultShouldDehydrateQuery(query) || - query.state.status === "pending", - }, - hydrate: { - deserializeData: SuperJSON.deserialize, - }, - }, - }); diff --git a/packages/cli/template/extras/src/trpc/react.tsx b/packages/cli/template/extras/src/trpc/react.tsx deleted file mode 100644 index ea02db57..00000000 --- a/packages/cli/template/extras/src/trpc/react.tsx +++ /dev/null @@ -1,76 +0,0 @@ -"use client"; - -import { type QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client"; -import { createTRPCReact } from "@trpc/react-query"; -import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; -import { useState } from "react"; -import SuperJSON from "superjson"; - -import type { AppRouter } from "~/server/api/root"; -import { createQueryClient } from "./query-client"; - -let clientQueryClientSingleton: QueryClient | undefined; -const getQueryClient = () => { - if (typeof window === "undefined") { - // Server: always make a new query client - return createQueryClient(); - } - // Browser: use singleton pattern to keep the same query client - return (clientQueryClientSingleton ??= createQueryClient()); -}; - -export const api = createTRPCReact(); - -/** - * Inference helper for inputs. - * - * @example type HelloInput = RouterInputs['example']['hello'] - */ -export type RouterInputs = inferRouterInputs; - -/** - * Inference helper for outputs. - * - * @example type HelloOutput = RouterOutputs['example']['hello'] - */ -export type RouterOutputs = inferRouterOutputs; - -export function TRPCReactProvider(props: { children: React.ReactNode }) { - const queryClient = getQueryClient(); - - const [trpcClient] = useState(() => - api.createClient({ - links: [ - loggerLink({ - enabled: (op) => - process.env.NODE_ENV === "development" || - (op.direction === "down" && op.result instanceof Error), - }), - unstable_httpBatchStreamLink({ - transformer: SuperJSON, - url: getBaseUrl() + "/api/trpc", - headers: () => { - const headers = new Headers(); - headers.set("x-trpc-source", "nextjs-react"); - return headers; - }, - }), - ], - }), - ); - - return ( - - - {props.children} - - - ); -} - -function getBaseUrl() { - if (typeof window !== "undefined") return window.location.origin; - if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; - return `http://localhost:${process.env.PORT ?? 3000}`; -} diff --git a/packages/cli/template/extras/src/trpc/server.ts b/packages/cli/template/extras/src/trpc/server.ts deleted file mode 100644 index 47acbe61..00000000 --- a/packages/cli/template/extras/src/trpc/server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import "server-only"; - -import { createHydrationHelpers } from "@trpc/react-query/rsc"; -import { headers } from "next/headers"; -import { cache } from "react"; - -import { type AppRouter, createCaller } from "~/server/api/root"; -import { createTRPCContext } from "~/server/api/trpc"; -import { createQueryClient } from "./query-client"; - -/** - * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when - * handling a tRPC call from a React Server Component. - */ -const createContext = cache(() => { - const heads = new Headers(headers()); - heads.set("x-trpc-source", "rsc"); - - return createTRPCContext({ - headers: heads, - }); -}); - -const getQueryClient = cache(createQueryClient); -const caller = createCaller(createContext); - -export const { trpc: api, HydrateClient } = createHydrationHelpers( - caller, - getQueryClient, -); diff --git a/packages/cli/template/extras/src/utils/api.ts b/packages/cli/template/extras/src/utils/api.ts deleted file mode 100644 index fc4f2398..00000000 --- a/packages/cli/template/extras/src/utils/api.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * This is the client-side entrypoint for your tRPC API. It is used to create the `api` object which - * contains the Next.js App-wrapper, as well as your type-safe React Query hooks. - * - * We also create a few inference helpers for input and output types. - */ -import { httpBatchLink, loggerLink } from "@trpc/client"; -import { createTRPCNext } from "@trpc/next"; -import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; -import superjson from "superjson"; - -import type { AppRouter } from "~/server/api/root"; - -const getBaseUrl = () => { - if (typeof window !== "undefined") return ""; // browser should use relative url - if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url - return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost -}; - -/** A set of type-safe react-query hooks for your tRPC API. */ -export const api = createTRPCNext({ - config() { - return { - /** - * Links used to determine request flow from client to server. - * - * @see https://trpc.io/docs/links - */ - links: [ - loggerLink({ - enabled: (opts) => - process.env.NODE_ENV === "development" || - (opts.direction === "down" && opts.result instanceof Error), - }), - httpBatchLink({ - /** - * Transformer used for data de-serialization from the server. - * - * @see https://trpc.io/docs/data-transformers - */ - transformer: superjson, - url: `${getBaseUrl()}/api/trpc`, - }), - ], - }; - }, - /** - * Whether tRPC should await queries when server rendering pages. - * - * @see https://trpc.io/docs/nextjs#ssr-boolean-default-false - */ - ssr: false, - transformer: superjson, -}); - -/** - * Inference helper for inputs. - * - * @example type HelloInput = RouterInputs['example']['hello'] - */ -export type RouterInputs = inferRouterInputs; - -/** - * Inference helper for outputs. - * - * @example type HelloOutput = RouterOutputs['example']['hello'] - */ -export type RouterOutputs = inferRouterOutputs; diff --git a/packages/cli/template/extras/start-database/mysql.sh b/packages/cli/template/extras/start-database/mysql.sh deleted file mode 100755 index 268df5cc..00000000 --- a/packages/cli/template/extras/start-database/mysql.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -# Use this script to start a docker container for a local development database - -# TO RUN ON WINDOWS: -# 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install -# 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/ -# 3. Open WSL - `wsl` -# 4. Run this script - `./start-database.sh` - -# On Linux and macOS you can run this script directly - `./start-database.sh` - -DB_CONTAINER_NAME="project1-mysql" - -if ! [ -x "$(command -v docker)" ]; then - echo -e "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/" - exit 1 -fi - -if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then - echo "Database container '$DB_CONTAINER_NAME' already running" - exit 0 -fi - -if [ "$(docker ps -q -a -f name=$DB_CONTAINER_NAME)" ]; then - docker start "$DB_CONTAINER_NAME" - echo "Existing database container '$DB_CONTAINER_NAME' started" - exit 0 -fi - -# import env variables from .env -set -a -source .env - -DB_PASSWORD=$(echo "$DATABASE_URL" | awk -F':' '{print $3}' | awk -F'@' '{print $1}') -DB_PORT=$(echo "$DATABASE_URL" | awk -F':' '{print $4}' | awk -F'\/' '{print $1}') - -if [ "$DB_PASSWORD" == "password" ]; then - echo "You are using the default database password" - read -p "Should we generate a random password for you? [y/N]: " -r REPLY - if ! [[ $REPLY =~ ^[Yy]$ ]]; then - echo "Please change the default password in the .env file and try again" - exit 1 - fi - # Generate a random URL-safe password - DB_PASSWORD=$(openssl rand -base64 12 | tr '+/' '-_') - sed -i -e "s#:password@#:$DB_PASSWORD@#" .env -fi - -docker run -d \ - --name $DB_CONTAINER_NAME \ - -e MYSQL_ROOT_PASSWORD="$DB_PASSWORD" \ - -e MYSQL_DATABASE=project1 \ - -p "$DB_PORT":3306 \ - docker.io/mysql && echo "Database container '$DB_CONTAINER_NAME' was successfully created" diff --git a/packages/cli/template/extras/start-database/postgres.sh b/packages/cli/template/extras/start-database/postgres.sh deleted file mode 100755 index 11fb2042..00000000 --- a/packages/cli/template/extras/start-database/postgres.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash -# Use this script to start a docker container for a local development database - -# TO RUN ON WINDOWS: -# 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install -# 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/ -# 3. Open WSL - `wsl` -# 4. Run this script - `./start-database.sh` - -# On Linux and macOS you can run this script directly - `./start-database.sh` - -DB_CONTAINER_NAME="project1-postgres" - -if ! [ -x "$(command -v docker)" ]; then - echo -e "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/" - exit 1 -fi - -if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then - echo "Database container '$DB_CONTAINER_NAME' already running" - exit 0 -fi - -if [ "$(docker ps -q -a -f name=$DB_CONTAINER_NAME)" ]; then - docker start "$DB_CONTAINER_NAME" - echo "Existing database container '$DB_CONTAINER_NAME' started" - exit 0 -fi - -# import env variables from .env -set -a -source .env - -DB_PASSWORD=$(echo "$DATABASE_URL" | awk -F':' '{print $3}' | awk -F'@' '{print $1}') -DB_PORT=$(echo "$DATABASE_URL" | awk -F':' '{print $4}' | awk -F'\/' '{print $1}') - -if [ "$DB_PASSWORD" = "password" ]; then - echo "You are using the default database password" - read -p "Should we generate a random password for you? [y/N]: " -r REPLY - if ! [[ $REPLY =~ ^[Yy]$ ]]; then - echo "Please change the default password in the .env file and try again" - exit 1 - fi - # Generate a random URL-safe password - DB_PASSWORD=$(openssl rand -base64 12 | tr '+/' '-_') - sed -i -e "s#:password@#:$DB_PASSWORD@#" .env -fi - -docker run -d \ - --name $DB_CONTAINER_NAME \ - -e POSTGRES_USER="postgres" \ - -e POSTGRES_PASSWORD="$DB_PASSWORD" \ - -e POSTGRES_DB=project1 \ - -p "$DB_PORT":5432 \ - docker.io/postgres && echo "Database container '$DB_CONTAINER_NAME' was successfully created" diff --git a/packages/cli/template/nextjs-shadcn/.claude/CLAUDE.md b/packages/cli/template/nextjs-shadcn/.claude/CLAUDE.md deleted file mode 100644 index 869eaac0..00000000 --- a/packages/cli/template/nextjs-shadcn/.claude/CLAUDE.md +++ /dev/null @@ -1,327 +0,0 @@ -# Project Context -Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter. - -## Key Principles -- Zero configuration required -- Subsecond performance -- Maximum type safety -- AI-friendly code generation - -## Before Writing Code -1. Analyze existing patterns in the codebase -2. Consider edge cases and error scenarios -3. Follow the rules below strictly -4. Validate accessibility requirements - -## Rules - -### Accessibility (a11y) -- Don't use `accessKey` attribute on any HTML element. -- Don't set `aria-hidden="true"` on focusable elements. -- Don't add ARIA roles, states, and properties to elements that don't support them. -- Don't use distracting elements like `` or ``. -- Only use the `scope` prop on `` elements. -- Don't assign non-interactive ARIA roles to interactive HTML elements. -- Make sure label elements have text content and are associated with an input. -- Don't assign interactive ARIA roles to non-interactive HTML elements. -- Don't assign `tabIndex` to non-interactive HTML elements. -- Don't use positive integers for `tabIndex` property. -- Don't include "image", "picture", or "photo" in img alt prop. -- Don't use explicit role property that's the same as the implicit/default role. -- Make static elements with click handlers use a valid role attribute. -- Always include a `title` element for SVG elements. -- Give all elements requiring alt text meaningful information for screen readers. -- Make sure anchors have content that's accessible to screen readers. -- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`. -- Include all required ARIA attributes for elements with ARIA roles. -- Make sure ARIA properties are valid for the element's supported roles. -- Always include a `type` attribute for button elements. -- Make elements with interactive roles and handlers focusable. -- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`). -- Always include a `lang` attribute on the html element. -- Always include a `title` attribute for iframe elements. -- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`. -- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`. -- Include caption tracks for audio and video elements. -- Use semantic elements instead of role attributes in JSX. -- Make sure all anchors are valid and navigable. -- Ensure all ARIA properties (`aria-*`) are valid. -- Use valid, non-abstract ARIA roles for elements with ARIA roles. -- Use valid ARIA state and property values. -- Use valid values for the `autocomplete` attribute on input elements. -- Use correct ISO language/country codes for the `lang` attribute. - -### Code Complexity and Quality -- Don't use consecutive spaces in regular expression literals. -- Don't use the `arguments` object. -- Don't use primitive type aliases or misleading types. -- Don't use the comma operator. -- Don't use empty type parameters in type aliases and interfaces. -- Don't write functions that exceed a given Cognitive Complexity score. -- Don't nest describe() blocks too deeply in test files. -- Don't use unnecessary boolean casts. -- Don't use unnecessary callbacks with flatMap. -- Use for...of statements instead of Array.forEach. -- Don't create classes that only have static members (like a static namespace). -- Don't use this and super in static contexts. -- Don't use unnecessary catch clauses. -- Don't use unnecessary constructors. -- Don't use unnecessary continue statements. -- Don't export empty modules that don't change anything. -- Don't use unnecessary escape sequences in regular expression literals. -- Don't use unnecessary fragments. -- Don't use unnecessary labels. -- Don't use unnecessary nested block statements. -- Don't rename imports, exports, and destructured assignments to the same name. -- Don't use unnecessary string or template literal concatenation. -- Don't use String.raw in template literals when there are no escape sequences. -- Don't use useless case statements in switch statements. -- Don't use ternary operators when simpler alternatives exist. -- Don't use useless `this` aliasing. -- Don't use any or unknown as type constraints. -- Don't initialize variables to undefined. -- Don't use the void operators (they're not familiar). -- Use arrow functions instead of function expressions. -- Use Date.now() to get milliseconds since the Unix Epoch. -- Use .flatMap() instead of map().flat() when possible. -- Use literal property access instead of computed property access. -- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work. -- Use concise optional chaining instead of chained logical expressions. -- Use regular expression literals instead of the RegExp constructor when possible. -- Don't use number literal object member names that aren't base 10 or use underscore separators. -- Remove redundant terms from logical expressions. -- Use while loops instead of for loops when you don't need initializer and update expressions. -- Don't pass children as props. -- Don't reassign const variables. -- Don't use constant expressions in conditions. -- Don't use `Math.min` and `Math.max` to clamp values when the result is constant. -- Don't return a value from a constructor. -- Don't use empty character classes in regular expression literals. -- Don't use empty destructuring patterns. -- Don't call global object properties as functions. -- Don't declare functions and vars that are accessible outside their block. -- Make sure builtins are correctly instantiated. -- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors. -- Don't use variables and function parameters before they're declared. -- Don't use 8 and 9 escape sequences in string literals. -- Don't use literal numbers that lose precision. - -### React and JSX Best Practices -- Don't use the return value of React.render. -- Make sure all dependencies are correctly specified in React hooks. -- Make sure all React hooks are called from the top level of component functions. -- Don't forget key props in iterators and collection literals. -- Don't destructure props inside JSX components in Solid projects. -- Don't define React components inside other components. -- Don't use event handlers on non-interactive elements. -- Don't assign to React component props. -- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element. -- Don't use dangerous JSX props. -- Don't use Array index in keys. -- Don't insert comments as text nodes. -- Don't assign JSX properties multiple times. -- Don't add extra closing tags for components without children. -- Use `<>...` instead of `...`. -- Watch out for possible "wrong" semicolons inside JSX elements. - -### Correctness and Safety -- Don't assign a value to itself. -- Don't return a value from a setter. -- Don't compare expressions that modify string case with non-compliant values. -- Don't use lexical declarations in switch clauses. -- Don't use variables that haven't been declared in the document. -- Don't write unreachable code. -- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass. -- Don't use control flow statements in finally blocks. -- Don't use optional chaining where undefined values aren't allowed. -- Don't have unused function parameters. -- Don't have unused imports. -- Don't have unused labels. -- Don't have unused private class members. -- Don't have unused variables. -- Make sure void (self-closing) elements don't have children. -- Don't return a value from a function with the return type 'void' -- Use isNaN() when checking for NaN. -- Make sure "for" loop update clauses move the counter in the right direction. -- Make sure typeof expressions are compared to valid values. -- Make sure generator functions contain yield. -- Don't use await inside loops. -- Don't use bitwise operators. -- Don't use expressions where the operation doesn't change the value. -- Make sure Promise-like statements are handled appropriately. -- Don't use __dirname and __filename in the global scope. -- Prevent import cycles. -- Don't use configured elements. -- Don't hardcode sensitive data like API keys and tokens. -- Don't let variable declarations shadow variables from outer scopes. -- Don't use the TypeScript directive @ts-ignore. -- Prevent duplicate polyfills from Polyfill.io. -- Don't use useless backreferences in regular expressions that always match empty strings. -- Don't use unnecessary escapes in string literals. -- Don't use useless undefined. -- Make sure getters and setters for the same property are next to each other in class and object definitions. -- Make sure object literals are declared consistently (defaults to explicit definitions). -- Use static Response methods instead of new Response() constructor when possible. -- Make sure switch-case statements are exhaustive. -- Make sure the `preconnect` attribute is used when using Google Fonts. -- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item. -- Make sure iterable callbacks return consistent values. -- Use `with { type: "json" }` for JSON module imports. -- Use numeric separators in numeric literals. -- Use object spread instead of `Object.assign()` when constructing new objects. -- Always use the radix argument when using `parseInt()`. -- Make sure JSDoc comment lines start with a single asterisk, except for the first one. -- Include a description parameter for `Symbol()`. -- Don't use spread (`...`) syntax on accumulators. -- Don't use the `delete` operator. -- Don't access namespace imports dynamically. -- Don't use namespace imports. -- Declare regex literals at the top level. -- Don't use `target="_blank"` without `rel="noopener"`. - -### TypeScript Best Practices -- Don't use TypeScript enums. -- Don't export imported variables. -- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions. -- Don't use TypeScript namespaces. -- Don't use non-null assertions with the `!` postfix operator. -- Don't use parameter properties in class constructors. -- Don't use user-defined types. -- Use `as const` instead of literal types and type annotations. -- Use either `T[]` or `Array` consistently. -- Initialize each enum member value explicitly. -- Use `export type` for types. -- Use `import type` for types. -- Make sure all enum members are literal values. -- Don't use TypeScript const enum. -- Don't declare empty interfaces. -- Don't let variables evolve into any type through reassignments. -- Don't use the any type. -- Don't misuse the non-null assertion operator (!) in TypeScript files. -- Don't use implicit any type on variable declarations. -- Don't merge interfaces and classes unsafely. -- Don't use overload signatures that aren't next to each other. -- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces. - -### Style and Consistency -- Don't use global `eval()`. -- Don't use callbacks in asynchronous tests and hooks. -- Don't use negation in `if` statements that have `else` clauses. -- Don't use nested ternary expressions. -- Don't reassign function parameters. -- This rule lets you specify global variable names you don't want to use in your application. -- Don't use specified modules when loaded by import or require. -- Don't use constants whose value is the upper-case version of their name. -- Use `String.slice()` instead of `String.substr()` and `String.substring()`. -- Don't use template literals if you don't need interpolation or special-character handling. -- Don't use `else` blocks when the `if` block breaks early. -- Don't use yoda expressions. -- Don't use Array constructors. -- Use `at()` instead of integer index access. -- Follow curly brace conventions. -- Use `else if` instead of nested `if` statements in `else` clauses. -- Use single `if` statements instead of nested `if` clauses. -- Use `new` for all builtins except `String`, `Number`, and `Boolean`. -- Use consistent accessibility modifiers on class properties and methods. -- Use `const` declarations for variables that are only assigned once. -- Put default function parameters and optional function parameters last. -- Include a `default` clause in switch statements. -- Use the `**` operator instead of `Math.pow`. -- Use `for-of` loops when you need the index to extract an item from the iterated array. -- Use `node:assert/strict` over `node:assert`. -- Use the `node:` protocol for Node.js builtin modules. -- Use Number properties instead of global ones. -- Use assignment operator shorthand where possible. -- Use function types instead of object types with call signatures. -- Use template literals over string concatenation. -- Use `new` when throwing an error. -- Don't throw non-Error values. -- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`. -- Use standard constants instead of approximated literals. -- Don't assign values in expressions. -- Don't use async functions as Promise executors. -- Don't reassign exceptions in catch clauses. -- Don't reassign class members. -- Don't compare against -0. -- Don't use labeled statements that aren't loops. -- Don't use void type outside of generic or return types. -- Don't use console. -- Don't use control characters and escape sequences that match control characters in regular expression literals. -- Don't use debugger. -- Don't assign directly to document.cookie. -- Use `===` and `!==`. -- Don't use duplicate case labels. -- Don't use duplicate class members. -- Don't use duplicate conditions in if-else-if chains. -- Don't use two keys with the same name inside objects. -- Don't use duplicate function parameter names. -- Don't have duplicate hooks in describe blocks. -- Don't use empty block statements and static blocks. -- Don't let switch clauses fall through. -- Don't reassign function declarations. -- Don't allow assignments to native objects and read-only global variables. -- Use Number.isFinite instead of global isFinite. -- Use Number.isNaN instead of global isNaN. -- Don't assign to imported bindings. -- Don't use irregular whitespace characters. -- Don't use labels that share a name with a variable. -- Don't use characters made with multiple code points in character class syntax. -- Make sure to use new and constructor properly. -- Don't use shorthand assign when the variable appears on both sides. -- Don't use octal escape sequences in string literals. -- Don't use Object.prototype builtins directly. -- Don't redeclare variables, functions, classes, and types in the same scope. -- Don't have redundant "use strict". -- Don't compare things where both sides are exactly the same. -- Don't let identifiers shadow restricted names. -- Don't use sparse arrays (arrays with holes). -- Don't use template literal placeholder syntax in regular strings. -- Don't use the then property. -- Don't use unsafe negation. -- Don't use var. -- Don't use with statements in non-strict contexts. -- Make sure async functions actually use await. -- Make sure default clauses in switch statements come last. -- Make sure to pass a message value when creating a built-in error. -- Make sure get methods always return a value. -- Use a recommended display strategy with Google Fonts. -- Make sure for-in loops include an if statement. -- Use Array.isArray() instead of instanceof Array. -- Make sure to use the digits argument with Number#toFixed(). -- Make sure to use the "use strict" directive in script files. - -### Next.js Specific Rules -- Don't use `` elements in Next.js projects. -- Don't use `` elements in Next.js projects. -- Don't import next/document outside of pages/_document.jsx in Next.js projects. -- Don't use the next/head module in pages/_document.js on Next.js projects. - -### Testing Best Practices -- Don't use export or module.exports in test files. -- Don't use focused tests. -- Make sure the assertion function, like expect, is placed inside an it() function call. -- Don't use disabled tests. - -## Common Tasks -- `npx ultracite init` - Initialize Ultracite in your project -- `npx ultracite format` - Format and fix code automatically -- `npx ultracite lint` - Check for issues without fixing - -## Example: Error Handling -```typescript -// โœ… Good: Comprehensive error handling -try { - const result = await fetchData(); - return { success: true, data: result }; -} catch (error) { - console.error('API call failed:', error); - return { success: false, error: error.message }; -} - -// โŒ Bad: Swallowing errors -try { - return await fetchData(); -} catch (e) { - console.log(e); -} -``` \ No newline at end of file diff --git a/packages/cli/template/nextjs-shadcn/.cursor/rules/ultracite.mdc b/packages/cli/template/nextjs-shadcn/.cursor/rules/ultracite.mdc deleted file mode 100644 index 98495535..00000000 --- a/packages/cli/template/nextjs-shadcn/.cursor/rules/ultracite.mdc +++ /dev/null @@ -1,333 +0,0 @@ ---- -description: Ultracite Rules - AI-Ready Formatter and Linter -globs: "**/*.{ts,tsx,js,jsx}" -alwaysApply: true ---- - -# Project Context -Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter. - -## Key Principles -- Zero configuration required -- Subsecond performance -- Maximum type safety -- AI-friendly code generation - -## Before Writing Code -1. Analyze existing patterns in the codebase -2. Consider edge cases and error scenarios -3. Follow the rules below strictly -4. Validate accessibility requirements - -## Rules - -### Accessibility (a11y) -- Don't use `accessKey` attribute on any HTML element. -- Don't set `aria-hidden="true"` on focusable elements. -- Don't add ARIA roles, states, and properties to elements that don't support them. -- Don't use distracting elements like `` or ``. -- Only use the `scope` prop on `` elements. -- Don't assign non-interactive ARIA roles to interactive HTML elements. -- Make sure label elements have text content and are associated with an input. -- Don't assign interactive ARIA roles to non-interactive HTML elements. -- Don't assign `tabIndex` to non-interactive HTML elements. -- Don't use positive integers for `tabIndex` property. -- Don't include "image", "picture", or "photo" in img alt prop. -- Don't use explicit role property that's the same as the implicit/default role. -- Make static elements with click handlers use a valid role attribute. -- Always include a `title` element for SVG elements. -- Give all elements requiring alt text meaningful information for screen readers. -- Make sure anchors have content that's accessible to screen readers. -- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`. -- Include all required ARIA attributes for elements with ARIA roles. -- Make sure ARIA properties are valid for the element's supported roles. -- Always include a `type` attribute for button elements. -- Make elements with interactive roles and handlers focusable. -- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`). -- Always include a `lang` attribute on the html element. -- Always include a `title` attribute for iframe elements. -- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`. -- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`. -- Include caption tracks for audio and video elements. -- Use semantic elements instead of role attributes in JSX. -- Make sure all anchors are valid and navigable. -- Ensure all ARIA properties (`aria-*`) are valid. -- Use valid, non-abstract ARIA roles for elements with ARIA roles. -- Use valid ARIA state and property values. -- Use valid values for the `autocomplete` attribute on input elements. -- Use correct ISO language/country codes for the `lang` attribute. - -### Code Complexity and Quality -- Don't use consecutive spaces in regular expression literals. -- Don't use the `arguments` object. -- Don't use primitive type aliases or misleading types. -- Don't use the comma operator. -- Don't use empty type parameters in type aliases and interfaces. -- Don't write functions that exceed a given Cognitive Complexity score. -- Don't nest describe() blocks too deeply in test files. -- Don't use unnecessary boolean casts. -- Don't use unnecessary callbacks with flatMap. -- Use for...of statements instead of Array.forEach. -- Don't create classes that only have static members (like a static namespace). -- Don't use this and super in static contexts. -- Don't use unnecessary catch clauses. -- Don't use unnecessary constructors. -- Don't use unnecessary continue statements. -- Don't export empty modules that don't change anything. -- Don't use unnecessary escape sequences in regular expression literals. -- Don't use unnecessary fragments. -- Don't use unnecessary labels. -- Don't use unnecessary nested block statements. -- Don't rename imports, exports, and destructured assignments to the same name. -- Don't use unnecessary string or template literal concatenation. -- Don't use String.raw in template literals when there are no escape sequences. -- Don't use useless case statements in switch statements. -- Don't use ternary operators when simpler alternatives exist. -- Don't use useless `this` aliasing. -- Don't use any or unknown as type constraints. -- Don't initialize variables to undefined. -- Don't use the void operators (they're not familiar). -- Use arrow functions instead of function expressions. -- Use Date.now() to get milliseconds since the Unix Epoch. -- Use .flatMap() instead of map().flat() when possible. -- Use literal property access instead of computed property access. -- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work. -- Use concise optional chaining instead of chained logical expressions. -- Use regular expression literals instead of the RegExp constructor when possible. -- Don't use number literal object member names that aren't base 10 or use underscore separators. -- Remove redundant terms from logical expressions. -- Use while loops instead of for loops when you don't need initializer and update expressions. -- Don't pass children as props. -- Don't reassign const variables. -- Don't use constant expressions in conditions. -- Don't use `Math.min` and `Math.max` to clamp values when the result is constant. -- Don't return a value from a constructor. -- Don't use empty character classes in regular expression literals. -- Don't use empty destructuring patterns. -- Don't call global object properties as functions. -- Don't declare functions and vars that are accessible outside their block. -- Make sure builtins are correctly instantiated. -- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors. -- Don't use variables and function parameters before they're declared. -- Don't use 8 and 9 escape sequences in string literals. -- Don't use literal numbers that lose precision. - -### React and JSX Best Practices -- Don't use the return value of React.render. -- Make sure all dependencies are correctly specified in React hooks. -- Make sure all React hooks are called from the top level of component functions. -- Don't forget key props in iterators and collection literals. -- Don't destructure props inside JSX components in Solid projects. -- Don't define React components inside other components. -- Don't use event handlers on non-interactive elements. -- Don't assign to React component props. -- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element. -- Don't use dangerous JSX props. -- Don't use Array index in keys. -- Don't insert comments as text nodes. -- Don't assign JSX properties multiple times. -- Don't add extra closing tags for components without children. -- Use `<>...` instead of `...`. -- Watch out for possible "wrong" semicolons inside JSX elements. - -### Correctness and Safety -- Don't assign a value to itself. -- Don't return a value from a setter. -- Don't compare expressions that modify string case with non-compliant values. -- Don't use lexical declarations in switch clauses. -- Don't use variables that haven't been declared in the document. -- Don't write unreachable code. -- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass. -- Don't use control flow statements in finally blocks. -- Don't use optional chaining where undefined values aren't allowed. -- Don't have unused function parameters. -- Don't have unused imports. -- Don't have unused labels. -- Don't have unused private class members. -- Don't have unused variables. -- Make sure void (self-closing) elements don't have children. -- Don't return a value from a function with the return type 'void' -- Use isNaN() when checking for NaN. -- Make sure "for" loop update clauses move the counter in the right direction. -- Make sure typeof expressions are compared to valid values. -- Make sure generator functions contain yield. -- Don't use await inside loops. -- Don't use bitwise operators. -- Don't use expressions where the operation doesn't change the value. -- Make sure Promise-like statements are handled appropriately. -- Don't use __dirname and __filename in the global scope. -- Prevent import cycles. -- Don't use configured elements. -- Don't hardcode sensitive data like API keys and tokens. -- Don't let variable declarations shadow variables from outer scopes. -- Don't use the TypeScript directive @ts-ignore. -- Prevent duplicate polyfills from Polyfill.io. -- Don't use useless backreferences in regular expressions that always match empty strings. -- Don't use unnecessary escapes in string literals. -- Don't use useless undefined. -- Make sure getters and setters for the same property are next to each other in class and object definitions. -- Make sure object literals are declared consistently (defaults to explicit definitions). -- Use static Response methods instead of new Response() constructor when possible. -- Make sure switch-case statements are exhaustive. -- Make sure the `preconnect` attribute is used when using Google Fonts. -- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item. -- Make sure iterable callbacks return consistent values. -- Use `with { type: "json" }` for JSON module imports. -- Use numeric separators in numeric literals. -- Use object spread instead of `Object.assign()` when constructing new objects. -- Always use the radix argument when using `parseInt()`. -- Make sure JSDoc comment lines start with a single asterisk, except for the first one. -- Include a description parameter for `Symbol()`. -- Don't use spread (`...`) syntax on accumulators. -- Don't use the `delete` operator. -- Don't access namespace imports dynamically. -- Don't use namespace imports. -- Declare regex literals at the top level. -- Don't use `target="_blank"` without `rel="noopener"`. - -### TypeScript Best Practices -- Don't use TypeScript enums. -- Don't export imported variables. -- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions. -- Don't use TypeScript namespaces. -- Don't use non-null assertions with the `!` postfix operator. -- Don't use parameter properties in class constructors. -- Don't use user-defined types. -- Use `as const` instead of literal types and type annotations. -- Use either `T[]` or `Array` consistently. -- Initialize each enum member value explicitly. -- Use `export type` for types. -- Use `import type` for types. -- Make sure all enum members are literal values. -- Don't use TypeScript const enum. -- Don't declare empty interfaces. -- Don't let variables evolve into any type through reassignments. -- Don't use the any type. -- Don't misuse the non-null assertion operator (!) in TypeScript files. -- Don't use implicit any type on variable declarations. -- Don't merge interfaces and classes unsafely. -- Don't use overload signatures that aren't next to each other. -- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces. - -### Style and Consistency -- Don't use global `eval()`. -- Don't use callbacks in asynchronous tests and hooks. -- Don't use negation in `if` statements that have `else` clauses. -- Don't use nested ternary expressions. -- Don't reassign function parameters. -- This rule lets you specify global variable names you don't want to use in your application. -- Don't use specified modules when loaded by import or require. -- Don't use constants whose value is the upper-case version of their name. -- Use `String.slice()` instead of `String.substr()` and `String.substring()`. -- Don't use template literals if you don't need interpolation or special-character handling. -- Don't use `else` blocks when the `if` block breaks early. -- Don't use yoda expressions. -- Don't use Array constructors. -- Use `at()` instead of integer index access. -- Follow curly brace conventions. -- Use `else if` instead of nested `if` statements in `else` clauses. -- Use single `if` statements instead of nested `if` clauses. -- Use `new` for all builtins except `String`, `Number`, and `Boolean`. -- Use consistent accessibility modifiers on class properties and methods. -- Use `const` declarations for variables that are only assigned once. -- Put default function parameters and optional function parameters last. -- Include a `default` clause in switch statements. -- Use the `**` operator instead of `Math.pow`. -- Use `for-of` loops when you need the index to extract an item from the iterated array. -- Use `node:assert/strict` over `node:assert`. -- Use the `node:` protocol for Node.js builtin modules. -- Use Number properties instead of global ones. -- Use assignment operator shorthand where possible. -- Use function types instead of object types with call signatures. -- Use template literals over string concatenation. -- Use `new` when throwing an error. -- Don't throw non-Error values. -- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`. -- Use standard constants instead of approximated literals. -- Don't assign values in expressions. -- Don't use async functions as Promise executors. -- Don't reassign exceptions in catch clauses. -- Don't reassign class members. -- Don't compare against -0. -- Don't use labeled statements that aren't loops. -- Don't use void type outside of generic or return types. -- Don't use console. -- Don't use control characters and escape sequences that match control characters in regular expression literals. -- Don't use debugger. -- Don't assign directly to document.cookie. -- Use `===` and `!==`. -- Don't use duplicate case labels. -- Don't use duplicate class members. -- Don't use duplicate conditions in if-else-if chains. -- Don't use two keys with the same name inside objects. -- Don't use duplicate function parameter names. -- Don't have duplicate hooks in describe blocks. -- Don't use empty block statements and static blocks. -- Don't let switch clauses fall through. -- Don't reassign function declarations. -- Don't allow assignments to native objects and read-only global variables. -- Use Number.isFinite instead of global isFinite. -- Use Number.isNaN instead of global isNaN. -- Don't assign to imported bindings. -- Don't use irregular whitespace characters. -- Don't use labels that share a name with a variable. -- Don't use characters made with multiple code points in character class syntax. -- Make sure to use new and constructor properly. -- Don't use shorthand assign when the variable appears on both sides. -- Don't use octal escape sequences in string literals. -- Don't use Object.prototype builtins directly. -- Don't redeclare variables, functions, classes, and types in the same scope. -- Don't have redundant "use strict". -- Don't compare things where both sides are exactly the same. -- Don't let identifiers shadow restricted names. -- Don't use sparse arrays (arrays with holes). -- Don't use template literal placeholder syntax in regular strings. -- Don't use the then property. -- Don't use unsafe negation. -- Don't use var. -- Don't use with statements in non-strict contexts. -- Make sure async functions actually use await. -- Make sure default clauses in switch statements come last. -- Make sure to pass a message value when creating a built-in error. -- Make sure get methods always return a value. -- Use a recommended display strategy with Google Fonts. -- Make sure for-in loops include an if statement. -- Use Array.isArray() instead of instanceof Array. -- Make sure to use the digits argument with Number#toFixed(). -- Make sure to use the "use strict" directive in script files. - -### Next.js Specific Rules -- Don't use `` elements in Next.js projects. -- Don't use `` elements in Next.js projects. -- Don't import next/document outside of pages/_document.jsx in Next.js projects. -- Don't use the next/head module in pages/_document.js on Next.js projects. - -### Testing Best Practices -- Don't use export or module.exports in test files. -- Don't use focused tests. -- Make sure the assertion function, like expect, is placed inside an it() function call. -- Don't use disabled tests. - -## Common Tasks -- `npx ultracite init` - Initialize Ultracite in your project -- `npx ultracite format` - Format and fix code automatically -- `npx ultracite lint` - Check for issues without fixing - -## Example: Error Handling -```typescript -// โœ… Good: Comprehensive error handling -try { - const result = await fetchData(); - return { success: true, data: result }; -} catch (error) { - console.error('API call failed:', error); - return { success: false, error: error.message }; -} - -// โŒ Bad: Swallowing errors -try { - return await fetchData(); -} catch (e) { - console.log(e); -} -``` \ No newline at end of file diff --git a/packages/cli/template/nextjs-shadcn/.vscode/settings.json b/packages/cli/template/nextjs-shadcn/.vscode/settings.json deleted file mode 100644 index 4175042a..00000000 --- a/packages/cli/template/nextjs-shadcn/.vscode/settings.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "editor.defaultFormatter": "esbenp.prettier-vscode", - "[javascript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[json]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[jsonc]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[css]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[graphql]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "typescript.tsdk": "node_modules/typescript/lib", - "editor.formatOnSave": true, - "editor.formatOnPaste": true, - "emmet.showExpandedAbbreviation": "never", - "editor.codeActionsOnSave": { - "source.fixAll.biome": "explicit", - "source.organizeImports.biome": "explicit" - } -} diff --git a/packages/cli/template/nextjs-shadcn/AGENTS.md b/packages/cli/template/nextjs-shadcn/AGENTS.md deleted file mode 100644 index 6b2924ba..00000000 --- a/packages/cli/template/nextjs-shadcn/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -__AGENT_INSTRUCTIONS__ diff --git a/packages/cli/template/nextjs-shadcn/CLAUDE.md b/packages/cli/template/nextjs-shadcn/CLAUDE.md deleted file mode 100644 index 43c994c2..00000000 --- a/packages/cli/template/nextjs-shadcn/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -@AGENTS.md diff --git a/packages/cli/template/nextjs-shadcn/README.md b/packages/cli/template/nextjs-shadcn/README.md deleted file mode 100644 index 80f5a13c..00000000 --- a/packages/cli/template/nextjs-shadcn/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# ProofKit NextJS Template - -This is a [NextJS](https://nextjs.org/) project bootstrapped with `@proofkit/cli`. Learn more at [proofkit.proof.sh](https://proofkit.proof.sh) - -## What's next? - -While this template is designed to be a minimal starting point, the ProofKit documentation will guide you through building your app. - -For more information, see the full [ProofKit documentation](https://proofkit.proof.sh). - -## Project Structure - -ProofKit projects have an opinionated structure to help you get started and some conventions must be maintained to ensure that the CLI can properly inject new features and components. - -The `src` directory is the home for your application code. It is used for most things except for configuration and is organized as follows: - -- `app` - NextJS app router, where your pages and routes are defined -- `components` - Shared components used throughout the app -- `server` - Code that connects to backend databases and services that should not be exposed in the browser - -Anytime you see an `internal` folder, you should not modify any files inside. These files are maintained exclusively by the ProofKit CLI and changes to them may be overwritten. - -Anytime you see a componet file that begins with `slot-`, you _may_ modify the content, but do not rename, remove, or move them. These are desigend to be customized, but are still used by the CLI to inject additional content. If a slot is not needed by your app, you can have the compoment return `null` or an empty fragment: `<>` diff --git a/packages/cli/template/nextjs-shadcn/_gitignore b/packages/cli/template/nextjs-shadcn/_gitignore deleted file mode 100644 index 1fdb37dc..00000000 --- a/packages/cli/template/nextjs-shadcn/_gitignore +++ /dev/null @@ -1,38 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -.pnpm-store -/.pnp -.pnp.js -.yarn/install-state.gz - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local -.env - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/packages/cli/template/nextjs-shadcn/components.json b/packages/cli/template/nextjs-shadcn/components.json deleted file mode 100644 index 5bcedb31..00000000 --- a/packages/cli/template/nextjs-shadcn/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/app/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} diff --git a/packages/cli/template/nextjs-shadcn/next.config.ts b/packages/cli/template/nextjs-shadcn/next.config.ts deleted file mode 100644 index 543e5a3c..00000000 --- a/packages/cli/template/nextjs-shadcn/next.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { NextConfig } from "next"; -import "@/lib/env"; - -const nextConfig: NextConfig = { - /* config options here */ -}; - -export default nextConfig; diff --git a/packages/cli/template/nextjs-shadcn/package.json b/packages/cli/template/nextjs-shadcn/package.json deleted file mode 100644 index ac00d978..00000000 --- a/packages/cli/template/nextjs-shadcn/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "raw-next", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev --turbopack", - "build": "next build --turbopack", - "start": "next start", - "typegen": "typegen", - "typegen:ui": "typegen ui", - "check": "ultracite check", - "fix": "ultracite fix", - "lint": "ultracite check .", - "format": "ultracite fix ." - }, - "dependencies": { - "@radix-ui/react-slot": "^1.2.3", - "@t3-oss/env-nextjs": "^0.13.8", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "lucide-react": "^0.541.0", - "next": "15.5.8", - "next-themes": "^0.4.6", - "radix-ui": "^1.4.2", - "react": "19.1.1", - "react-dom": "19.1.1", - "sonner": "^2.0.4", - "tailwind-merge": "^3.3.1" - }, - "devDependencies": { - "@proofkit/typegen": "^1.1.1", - "@tailwindcss/postcss": "^4", - "@types/node": "^22", - "@types/react": "^19", - "@types/react-dom": "^19", - "oxlint": "^1.39.0", - "tailwindcss": "^4", - "tw-animate-css": "^1.3.7", - "typescript": "^5", - "ultracite": "^7.0.0" - } -} diff --git a/packages/cli/template/nextjs-shadcn/postcss.config.mjs b/packages/cli/template/nextjs-shadcn/postcss.config.mjs deleted file mode 100644 index f50127cd..00000000 --- a/packages/cli/template/nextjs-shadcn/postcss.config.mjs +++ /dev/null @@ -1,5 +0,0 @@ -const config = { - plugins: ["@tailwindcss/postcss"], -}; - -export default config; diff --git a/packages/cli/template/nextjs-shadcn/proofkit.json b/packages/cli/template/nextjs-shadcn/proofkit.json deleted file mode 100644 index 69c00095..00000000 --- a/packages/cli/template/nextjs-shadcn/proofkit.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "ui": "shadcn", - "envFile": ".env", - "appType": "browser", - "registryTemplates": ["utils/t3-env"] -} diff --git a/packages/cli/template/nextjs-shadcn/public/favicon.ico b/packages/cli/template/nextjs-shadcn/public/favicon.ico deleted file mode 100644 index ba9355b8..00000000 Binary files a/packages/cli/template/nextjs-shadcn/public/favicon.ico and /dev/null differ diff --git a/packages/cli/template/nextjs-shadcn/public/proofkit.png b/packages/cli/template/nextjs-shadcn/public/proofkit.png deleted file mode 100644 index 2969f842..00000000 Binary files a/packages/cli/template/nextjs-shadcn/public/proofkit.png and /dev/null differ diff --git a/packages/cli/template/nextjs-shadcn/src/app/(main)/layout.tsx b/packages/cli/template/nextjs-shadcn/src/app/(main)/layout.tsx deleted file mode 100644 index d9f86ecb..00000000 --- a/packages/cli/template/nextjs-shadcn/src/app/(main)/layout.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import type React from "react"; -import AppShell from "@/components/AppShell/internal/AppShell"; - -export default function Layout({ children }: { children: React.ReactNode }) { - return {children}; -} diff --git a/packages/cli/template/nextjs-shadcn/src/app/(main)/page.tsx b/packages/cli/template/nextjs-shadcn/src/app/(main)/page.tsx deleted file mode 100644 index 979764ca..00000000 --- a/packages/cli/template/nextjs-shadcn/src/app/(main)/page.tsx +++ /dev/null @@ -1,93 +0,0 @@ -"use client"; - -import { - ExternalLinkIcon, -} from "lucide-react"; -import { Button } from "@/components/ui/button"; - -function GitHubMark({ size = 20 }: { size?: number }) { - return ( - - ); -} - -export default function Home() { - return ( -
-
-
- ProofKit -

Welcome!

- -

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

- -

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

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

- React + TypeScript + Vite -

-

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

-

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

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

{step.title}

-

- {step.body} -

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

- React Query ready -

-

- TanStack Query is preconfigured -

-

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

- -
- {hintQuery.isLoading ? "Loading starter data..." : hintQuery.data} -
- -
- - Back to starter - -
-
-
- ); -}; diff --git a/packages/cli/template/vite-wv/tsconfig.json b/packages/cli/template/vite-wv/tsconfig.json deleted file mode 100644 index 46fa82ef..00000000 --- a/packages/cli/template/vite-wv/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "esModuleInterop": true, - "jsx": "react-jsx", - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "noEmit": true, - "paths": { - "@/*": ["./src/*"], - }, - }, - "include": ["src"], -} diff --git a/packages/cli/template/vite-wv/vite.config.ts b/packages/cli/template/vite-wv/vite.config.ts deleted file mode 100644 index 670394a7..00000000 --- a/packages/cli/template/vite-wv/vite.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import path from "node:path"; - -import { fmBridge } from "@proofkit/webviewer/vite-plugins"; -import tailwindcss from "@tailwindcss/vite"; -import react from "@vitejs/plugin-react"; -import { defineConfig } from "vite"; -import { viteSingleFile } from "vite-plugin-singlefile"; - -const currentDirectory = import.meta.dirname; - -export default defineConfig({ - plugins: [fmBridge(), react(), tailwindcss(), viteSingleFile()], - resolve: { - alias: { - "@": path.resolve(currentDirectory, "./src"), - }, - }, - server: { - port: 5175, - }, -}); diff --git a/packages/cli/tests/browser-apps.smoke.test.ts b/packages/cli/tests/browser-apps.smoke.test.ts deleted file mode 100644 index b2ddb1b2..00000000 --- a/packages/cli/tests/browser-apps.smoke.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { execSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { beforeEach, describe, expect, it } from "vitest"; -import { z } from "zod/v4"; - -import { verifySmokeProjectBuilds } from "./test-utils.js"; - -const smokeEnvSchema = z.object({ - OTTO_SERVER_URL: z.url(), - OTTO_ADMIN_API_KEY: z.string().min(1), - FM_DATA_API_KEY: z.string().min(1), - FM_FILE_NAME: z.string().min(1), -}); - -const parsedSmokeEnv = smokeEnvSchema.safeParse(process.env); -const describeWhenSmokeEnvPresent = parsedSmokeEnv.success ? describe : describe.skip; - -if (!parsedSmokeEnv.success) { - const missingKeys = [...new Set(parsedSmokeEnv.error.issues.map((issue) => issue.path.join(".")))]; - console.warn(`Skipping external integration smoke tests; missing required env vars: ${missingKeys.join(", ")}`); -} - -describeWhenSmokeEnvPresent("External integration smoke tests (non-interactive CLI)", () => { - if (!parsedSmokeEnv.success) { - return; - } - - const testDir = join(tmpdir(), "proofkit-cli-tests"); - const cliPath = join(import.meta.dirname, "..", "bin", "proofkit.cjs"); - const projectName = "test-fm-project"; - const projectDir = join(testDir, projectName); - - // Required for live Otto/FileMaker integration smoke coverage. - const testEnv = parsedSmokeEnv.data; - - beforeEach( - () => { - // Clean up any stale test project from previous runs - if (existsSync(projectDir)) { - rmSync(projectDir, { recursive: true, force: true }); - } - // Ensure the test directory exists - mkdirSync(testDir, { recursive: true }); - }, - 30_000, // 30s timeout for cleanup of large node_modules - ); - - it("should create a browser project with FileMaker integration in non-interactive mode", () => { - // Build the command with all necessary flags for non-interactive mode - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--app-type browser", - "--data-source filemaker", - `--server "${testEnv.OTTO_SERVER_URL}"`, - `--admin-api-key "${testEnv.OTTO_ADMIN_API_KEY}"`, - `--data-api-key "${testEnv.FM_DATA_API_KEY}"`, - `--file-name "${testEnv.FM_FILE_NAME}"`, - "--no-git", // Skip git initialization for testing - "--no-install", // Release smoke runs before the new CLI version is published. - ].join(" "); - - // Execute the command - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - }); - }).not.toThrow(); - - // Verify project structure - expect(existsSync(projectDir)).toBe(true); - expect(existsSync(join(projectDir, "package.json"))).toBe(true); - expect(existsSync(join(projectDir, "proofkit.json"))).toBe(true); - expect(existsSync(join(projectDir, ".env"))).toBe(true); - - // Verify package.json content - const pkgJson = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf-8")); - expect(pkgJson.name).toBe(projectName); - - // Verify proofkit.json content - const proofkitConfig = JSON.parse(readFileSync(join(projectDir, "proofkit.json"), "utf-8")); - expect(proofkitConfig.appType).toBe("browser"); - expect(proofkitConfig.dataSources).toContainEqual( - expect.objectContaining({ - type: "fm", - name: "filemaker", - }), - ); - - // Verify the project can be built successfully - verifySmokeProjectBuilds(projectDir); - }); -}); diff --git a/packages/cli/tests/cli.test.ts b/packages/cli/tests/cli.test.ts deleted file mode 100644 index 52a4cb81..00000000 --- a/packages/cli/tests/cli.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { execFileSync, spawnSync } from "node:child_process"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; -import fs from "fs-extra"; -import { describe, expect, it } from "vitest"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const packageDir = path.join(__dirname, ".."); -const distEntry = path.join(packageDir, "bin/proofkit.cjs"); -const semverOutputPattern = /^\d+\.\d+\.\d+(?:-[\w.]+)?\n$/; - -describe("proofkit CLI", () => { - it("prints raw version with -v", () => { - const output = execFileSync("node", [distEntry, "-v"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - expect(output).toMatch(semverOutputPattern); - expect(output).not.toContain("_______"); - expect(output).not.toContain("ProofKit"); - }); - - it("shows kebab-case init flags in help", () => { - const output = execFileSync("node", [distEntry, "init", "--help"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - expect(output).toContain("--app-type"); - expect(output).toContain("--proofkit-token"); - expect(output).toContain("--non-interactive"); - expect(output).toContain("--no-install"); - expect(output).toContain("--no-git"); - expect(output).not.toContain("--ui"); - expect(output).not.toContain("--appType"); - }); - - it("prints the header and project command guidance when run inside a ProofKit project", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-cli-project-")); - await fs.writeJson(path.join(cwd, "proofkit.json"), { - appType: "browser", - ui: "shadcn", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], - }); - - const output = execFileSync("node", [distEntry], { - cwd, - stdio: "pipe", - encoding: "utf8", - }); - - expect(output).toContain("_______"); - expect(output).toContain("Found"); - expect(output).toContain("Project commands"); - expect(output).toContain("proofkit doctor"); - expect(output).toContain("proofkit prompt"); - }); - - it("fails with guidance when no command is used in non-interactive mode", () => { - const result = spawnSync("node", [distEntry, "--non-interactive"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - expect(result.status).not.toBe(0); - expect(`${result.stdout}\n${result.stderr}`).toContain("interactive-only in non-interactive mode"); - expect(`${result.stdout}\n${result.stderr}`).toContain("proofkit init --non-interactive"); - }); - - it("auto-detects piped execution as non-interactive", () => { - const result = spawnSync("node", [distEntry], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - expect(result.status).not.toBe(0); - expect(`${result.stdout}\n${result.stderr}`).toContain("interactive-only in non-interactive mode"); - }); - - it("runs when invoked through a symlinked bin path", async () => { - const shimDir = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-cli-shim-")); - const shimPath = path.join(shimDir, "proofkit"); - await fs.symlink(distEntry, shimPath); - - const result = spawnSync("node", [shimPath, "init", "--help"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - expect(result.status).toBe(0); - expect(result.stdout).toContain("ProofKit"); - expect(result.stdout).toContain("Create a new project with ProofKit"); - expect(result.stdout).toContain("--app-type"); - expect(result.stdout).not.toContain("--ui"); - }); - - it("parses init proofkit token flag", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-cli-token-")); - - const result = spawnSync( - "node", - [ - distEntry, - "init", - "token-app", - "--app-type", - "browser", - "--data-source", - "none", - "--non-interactive", - "--no-install", - "--no-git", - "--proofkit-token", - "parser-token", - ], - { - cwd, - stdio: "pipe", - encoding: "utf8", - env: { - ...process.env, - PROOFKIT_DISABLE_BUNDLED_BINARY: "1", - }, - }, - ); - - expect(result.status).toBe(0); - expect(await fs.pathExists(path.join(cwd, "token-app", "proofkit.json"))).toBe(true); - }); - - it("shows a clean invalid subcommand error by default", () => { - const result = spawnSync("node", [distEntry, "my-proofkit-app", "--force"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).not.toBe(0); - expect(output).toContain( - "Invalid subcommand for proofkit - use one of 'init', 'doctor', 'prompt', '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", () => { - const result = spawnSync("node", [distEntry, "--debug", "my-proofkit-app", "--force"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).not.toBe(0); - expect(output).toContain( - "Invalid subcommand for proofkit - use one of 'init', 'doctor', 'prompt', 'add', 'remove', 'typegen', 'deploy', 'upgrade'", - ); - expect(output).toContain("[debug]"); - expect(output).toContain('"CommandMismatch"'); - }); - - it("supports `proofkit prompt`", () => { - const result = spawnSync("node", [distEntry, "prompt"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - expect(result.status).toBe(0); - expect(`${result.stdout}\n${result.stderr}`).toContain("Agent-ready prompts are coming soon."); - }); - - it("supports `proofkit add addon webviewer`", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-cli-addon-project-")); - const addonDownloadDir = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-cli-addon-downloads-")); - const fixtureDir = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-cli-addon-fixtures-")); - const addonFixturePath = path.join(fixtureDir, "ProofKit.fmaddon"); - await fs.writeFile(addonFixturePath, "fake-fmaddon"); - const manifestPath = path.join(fixtureDir, "manifest.json"); - await fs.writeJson(manifestPath, { - latestVersion: "2.2.4.0", - versions: [ - { - version: "2.2.4.0", - assets: [ - { - file: "ProofKit.fmaddon", - url: pathToFileURL(addonFixturePath).toString(), - }, - ], - }, - ], - }); - - const result = spawnSync("node", [distEntry, "add", "addon", "webviewer", "--non-interactive"], { - cwd, - stdio: "pipe", - encoding: "utf8", - env: { - ...process.env, - PROOFKIT_FM_ADDON_MANIFEST_URL: pathToFileURL(manifestPath).toString(), - PROOFKIT_FM_ADDON_DOWNLOAD_DIR: addonDownloadDir, - PROOFKIT_SKIP_OPEN_FM_ADDON: "1", - }, - }); - - expect(result.status).toBe(0); - - expect(await fs.pathExists(path.join(addonDownloadDir, "ProofKit.fmaddon"))).toBe(true); - expect(await fs.pathExists(path.join(addonDownloadDir, "ProofKit.fmaddon.proofkit.json"))).toBe(true); - }); - - it("rejects unsupported add targets", () => { - const result = spawnSync("node", [distEntry, "add", "page"], { - cwd: packageDir, - stdio: "pipe", - encoding: "utf8", - }); - - expect(result.status).not.toBe(0); - expect(`${result.stdout}\n${result.stderr}`).toContain("Only `proofkit add addon ` is supported."); - }); -}); diff --git a/packages/cli/tests/default-command.test.ts b/packages/cli/tests/default-command.test.ts deleted file mode 100644 index 98d578ec..00000000 --- a/packages/cli/tests/default-command.test.ts +++ /dev/null @@ -1,169 +0,0 @@ -import os from "node:os"; -import path from "node:path"; -import { Effect } from "effect"; -import fs from "fs-extra"; -import { describe, expect, it } from "vitest"; -import { NonInteractiveInputError, UserCancelledError } from "~/core/errors.js"; -import { runDefaultCommand } from "~/index.js"; -import { getFailure } from "./effect-test-utils.js"; -import { makeTestLayer } from "./test-layer.js"; - -function createConsoleTranscript() { - return { - info: [] as string[], - warn: [] as string[], - error: [] as string[], - success: [] as string[], - note: [] as Array<{ message: string; title?: string }>, - }; -} - -describe("default command routing", () => { - it("routes to init when no ProofKit project is present", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-default-init-")); - const consoleTranscript = createConsoleTranscript(); - - await Effect.runPromise( - runDefaultCommand().pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - nonInteractive: false, - console: consoleTranscript, - prompts: { - text: ["routed-app"], - select: ["browser", "none"], - }, - }), - ), - ); - - expect(await fs.pathExists(path.join(cwd, "routed-app", "proofkit.json"))).toBe(true); - expect(consoleTranscript.success.at(-1) ?? "").toContain("Created routed-app"); - expect(consoleTranscript.note.some((entry) => entry.title === "Coming soon")).toBe(false); - }); - - it("shows the project menu when a ProofKit project is present in interactive mode", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-default-project-")); - await fs.writeJson(path.join(cwd, "proofkit.json"), { - appType: "browser", - ui: "shadcn", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], - }); - const consoleTranscript = createConsoleTranscript(); - const promptTranscript = { - text: [], - password: [], - select: [], - searchSelect: [], - multiSearchSelect: [], - confirm: [], - }; - - await Effect.runPromise( - runDefaultCommand().pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - nonInteractive: false, - console: consoleTranscript, - prompts: { - select: ["doctor"], - }, - promptTranscript, - }), - ), - ); - - expect(promptTranscript.select).toEqual([ - { - message: "What would you like to do?", - options: ["add", "remove", "typegen", "deploy", "upgrade", "doctor", "prompt", "docs"], - }, - ]); - expect(consoleTranscript.note.some((entry) => entry.title === "Project commands")).toBe(false); - }); - - it("preserves user cancellation from the project menu prompt", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-default-project-cancel-")); - await fs.writeJson(path.join(cwd, "proofkit.json"), { - appType: "browser", - ui: "shadcn", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], - }); - - expect( - await getFailure( - runDefaultCommand().pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - nonInteractive: false, - prompts: { - select: ["__cancel__"], - }, - }), - ), - ), - ).toMatchObject( - new UserCancelledError({ - message: "User aborted the operation", - }), - ); - }); - - it("shows explicit project command guidance when a ProofKit project is present in non-interactive mode", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-default-project-ci-")); - await fs.writeJson(path.join(cwd, "proofkit.json"), { - appType: "browser", - ui: "shadcn", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], - }); - const consoleTranscript = createConsoleTranscript(); - - await Effect.runPromise( - runDefaultCommand({ nonInteractive: true }).pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - nonInteractive: true, - console: consoleTranscript, - }), - ), - ); - - expect(consoleTranscript.note).toEqual([ - { - title: "Project commands", - message: expect.stringContaining("Use an explicit command such as `proofkit doctor`"), - }, - ]); - }); - - it("fails in non-interactive mode without an explicit command", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-default-ci-")); - - expect( - await getFailure( - runDefaultCommand({ nonInteractive: true }).pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - nonInteractive: true, - }), - ), - ), - ).toMatchObject( - new NonInteractiveInputError({ - message: - "The default command is interactive-only in non-interactive mode. Run an explicit command such as `proofkit init --non-interactive`.", - }), - ); - }); -}); diff --git a/packages/cli/tests/doctor.test.ts b/packages/cli/tests/doctor.test.ts deleted file mode 100644 index d9c7bbb4..00000000 --- a/packages/cli/tests/doctor.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import os from "node:os"; -import path from "node:path"; -import { Effect } from "effect"; -import fs from "fs-extra"; -import { describe, expect, it } from "vitest"; -import { runDoctor } from "~/core/doctor.js"; -import { runPrompt } from "~/core/prompt.js"; -import { makeTestLayer } from "./test-layer.js"; - -function createConsoleTranscript() { - return { - info: [] as string[], - warn: [] as string[], - error: [] as string[], - success: [] as string[], - note: [] as Array<{ message: string; title?: string }>, - }; -} - -describe("doctor and prompt commands", () => { - it("reports missing proofkit project", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-doctor-missing-")); - const consoleTranscript = createConsoleTranscript(); - - await Effect.runPromise( - runDoctor.pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - console: consoleTranscript, - }), - ), - ); - - expect(consoleTranscript.note[0]?.title).toBe("Doctor"); - expect(consoleTranscript.note[0]?.message).toContain("No ProofKit project found"); - expect(consoleTranscript.note[0]?.message).toContain("proofkit init"); - }); - - it("reports missing typegen config and package-native next steps", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-doctor-project-")); - const consoleTranscript = createConsoleTranscript(); - - await fs.writeJson(path.join(cwd, "proofkit.json"), { - appType: "browser", - ui: "shadcn", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], - }); - await fs.writeJson(path.join(cwd, "package.json"), { - name: "doctor-test", - scripts: { - typegen: "npx @proofkit/typegen", - }, - devDependencies: { - "@proofkit/typegen": "workspace:*", - }, - }); - - await Effect.runPromise( - runDoctor.pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - console: consoleTranscript, - }), - ), - ); - - expect(consoleTranscript.note[0]?.title).toBe("Doctor"); - expect(consoleTranscript.note[0]?.message).toContain("Missing `proofkit-typegen.config.jsonc`"); - expect(consoleTranscript.note[0]?.message).toContain("npx @proofkit/typegen init"); - expect(consoleTranscript.note[0]?.message).toContain("npx @proofkit/typegen ui"); - }); - - it("returns coming-soon messaging for prompt", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-prompt-")); - const consoleTranscript = createConsoleTranscript(); - - await Effect.runPromise( - runPrompt.pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - console: consoleTranscript, - }), - ), - ); - - expect(consoleTranscript.note[0]?.title).toBe("Coming soon"); - expect(consoleTranscript.note[0]?.message).toContain("Agent-ready prompts are coming soon."); - }); -}); diff --git a/packages/cli/tests/effect-test-utils.ts b/packages/cli/tests/effect-test-utils.ts deleted file mode 100644 index c92f65e2..00000000 --- a/packages/cli/tests/effect-test-utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Effect as Fx } from "effect"; -import { Cause, Effect, Exit } from "effect"; -import { getOrUndefined } from "effect/Option"; - -export async function getFailure(effect: Fx.Effect) { - const exit = await Effect.runPromiseExit(effect); - if (!Exit.isFailure(exit)) { - throw new Error("Expected effect to fail."); - } - const failure = getOrUndefined(Cause.failureOption(exit.cause)); - if (!failure) { - throw new Error("Expected failure cause."); - } - return failure; -} diff --git a/packages/cli/tests/executor.test.ts b/packages/cli/tests/executor.test.ts deleted file mode 100644 index a86c1712..00000000 --- a/packages/cli/tests/executor.test.ts +++ /dev/null @@ -1,787 +0,0 @@ -import os from "node:os"; -import path from "node:path"; -import { Effect } from "effect"; -import fs from "fs-extra"; -import { describe, expect, it } from "vitest"; -import { DirectoryConflictError, ExternalCommandError, UserCancelledError } from "~/core/errors.js"; -import { executeInitPlan } from "~/core/executeInitPlan.js"; -import { planInit } from "~/core/planInit.js"; -import { getFailure } from "./effect-test-utils.js"; -import { getSharedTemplateDir, makeInitRequest, readScaffoldArtifacts } from "./init-fixtures.js"; -import { makeTestLayer } from "./test-layer.js"; - -describe("executeInitPlan command paths", () => { - it("runs install, git, codegen, and filemaker bootstrap through services", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-exec-")); - const tracker = { - commands: [] as string[], - gitInits: 0, - codegens: 0, - filemakerBootstraps: 0, - }; - - const plan = planInit( - makeInitRequest({ - projectName: "fm-app", - scopedAppName: "fm-app", - appDir: "fm-app", - appType: "webviewer", - ui: "shadcn", - dataSource: "filemaker", - packageManager: "pnpm", - noInstall: false, - noGit: false, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: true, - fileMaker: { - mode: "hosted-otto", - dataSourceName: "filemaker", - envNames: { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }, - server: "https://example.com", - fileName: "Contacts.fmp12", - dataApiKey: "dk_123", - layoutName: "API_Contacts", - schemaName: "Contacts", - }, - }), - { - templateDir: getSharedTemplateDir("vite-wv"), - packageManagerVersion: "11.0.0", - }, - ); - - await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "pnpm", tracker }))); - - expect(tracker.commands).toEqual([ - "pnpm install", - [ - "pnpx ultracite@^7 init --quiet --linter oxlint --pm pnpm --frameworks react --editors cursor", - "--agents claude codex --hooks cursor windsurf", - ].join(" "), - "pnpx @tanstack/intent@latest install", - "pnpm fix", - "pnpm lint", - ]); - expect(tracker.filemakerBootstraps).toBe(1); - expect(tracker.codegens).toBe(1); - expect(tracker.gitInits).toBe(1); - - const { proofkitJson, envFile, typegenConfig, pnpmWorkspaceFile } = await readScaffoldArtifacts( - path.join(cwd, "fm-app"), - ); - expect(proofkitJson.dataSources).toHaveLength(1); - expect(envFile).toContain("FM_DATABASE=Contacts.fmp12"); - expect(typegenConfig).toContain("API_Contacts"); - expect(typegenConfig).toContain("Contacts"); - expect(pnpmWorkspaceFile).toContain("trustPolicy: no-downgrade"); - expect(pnpmWorkspaceFile).toContain("trustPolicyIgnoreAfter: 43200"); - expect(pnpmWorkspaceFile).toContain("blockExoticSubdeps: true"); - }); - - it("writes browser Ultracite config without external init in no-install mode", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-ultracite-browser-")); - const tracker = { - commands: [] as string[], - gitInits: 0, - codegens: 0, - filemakerBootstraps: 0, - }; - - const plan = planInit( - makeInitRequest({ - appType: "browser", - dataSource: "none", - packageManager: "npm", - noInstall: true, - noGit: true, - cwd, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "npm", tracker }))); - - const { huskyPreCommitFile, npmrcFile } = await readScaffoldArtifacts(path.join(cwd, "demo-app")); - - expect(tracker.commands).toEqual([]); - expect(huskyPreCommitFile).toContain("pnpm exec lint-staged"); - expect(npmrcFile).toContain("min-release-age=1"); - }); - - it("warns and continues when final lint fails", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-lint-fix-warn-")); - const console = { - error: [] as string[], - info: [] as string[], - note: [] as Array<{ message: string; title?: string }>, - success: [] as string[], - warn: [] as string[], - }; - const tracker = { - commands: [] as string[], - gitInits: 0, - codegens: 0, - filemakerBootstraps: 0, - }; - - const plan = planInit( - makeInitRequest({ - appType: "webviewer", - dataSource: "none", - packageManager: "pnpm", - noInstall: false, - noGit: true, - cwd, - }), - { - templateDir: getSharedTemplateDir("vite-wv"), - packageManagerVersion: "11.0.0", - }, - ); - - await Effect.runPromise( - executeInitPlan(plan).pipe( - makeTestLayer({ - console, - cwd, - failProcessCommand: "pnpm lint", - failures: { - processRun: new ExternalCommandError({ - args: ["lint"], - command: "pnpm", - cwd, - message: "lint failed", - }), - }, - packageManager: "pnpm", - tracker, - }), - ), - ); - - expect(tracker.commands).toContain("pnpm fix"); - expect(tracker.commands).toContain("pnpm lint"); - expect(console.warn).toContain("Lint did not succeed; continuing setup."); - }); - - it("supports force overwrite for an existing directory", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-force-")); - const projectDir = path.join(cwd, "force-app"); - await fs.ensureDir(projectDir); - await fs.writeFile(path.join(projectDir, "README.md"), "old content"); - - const plan = planInit( - makeInitRequest({ - projectName: "force-app", - scopedAppName: "force-app", - appDir: "force-app", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: true, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "pnpm" }))); - - expect(await fs.pathExists(path.join(projectDir, "README.md"))).toBe(true); - expect(await fs.readFile(path.join(projectDir, "README.md"), "utf8")).not.toBe("old content"); - }); - - it("persists selected local MCP file into typegen config", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-local-mcp-explicit-")); - - const plan = planInit( - makeInitRequest({ - projectName: "local-mcp-app", - scopedAppName: "local-mcp-app", - appDir: "local-mcp-app", - appType: "webviewer", - ui: "shadcn", - dataSource: "filemaker", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: true, - fileMaker: { - mode: "local-fm-mcp", - dataSourceName: "filemaker", - envNames: { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }, - fmMcpBaseUrl: "http://127.0.0.1:1365", - fileName: "Selected.fmp12", - }, - }), - { - templateDir: getSharedTemplateDir("vite-wv"), - }, - ); - - await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "pnpm" }))); - - const { typegenConfig } = await readScaffoldArtifacts(path.join(cwd, "local-mcp-app")); - expect(typegenConfig).toContain('"connectedFileName": "Selected.fmp12"'); - }); - - it("does not persist local MCP token into typegen config", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-local-mcp-token-config-")); - - const plan = planInit( - makeInitRequest({ - projectName: "local-mcp-token-app", - scopedAppName: "local-mcp-token-app", - appDir: "local-mcp-token-app", - appType: "webviewer", - ui: "shadcn", - dataSource: "filemaker", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - proofkitToken: "secret-session-token", - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: true, - fileMaker: { - mode: "local-fm-mcp", - dataSourceName: "filemaker", - envNames: { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }, - fmMcpBaseUrl: "http://127.0.0.1:1365", - fileName: "Selected.fmp12", - proofkitToken: "secret-session-token", - }, - }), - { - templateDir: getSharedTemplateDir("vite-wv"), - }, - ); - - await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "pnpm" }))); - - const { typegenConfig } = await readScaffoldArtifacts(path.join(cwd, "local-mcp-token-app")); - expect(typegenConfig).not.toContain("secret-session-token"); - expect(typegenConfig).not.toContain("proofkitToken"); - }); - - it("persists the single auto-selected local MCP file into typegen config", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-local-mcp-single-")); - - const tracker = { - commands: [] as string[], - gitInits: 0, - codegens: 0, - codegenTokens: [] as Array, - filemakerBootstraps: 0, - }; - const plan = planInit( - makeInitRequest({ - projectName: "single-local-mcp-app", - scopedAppName: "single-local-mcp-app", - appDir: "single-local-mcp-app", - appType: "webviewer", - ui: "shadcn", - dataSource: "filemaker", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - proofkitToken: "initial-codegen-token", - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - fileMaker: { - mode: "local-fm-mcp", - dataSourceName: "filemaker", - envNames: { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }, - fmMcpBaseUrl: "http://127.0.0.1:1365", - fileName: "OnlyOpen.fmp12", - }, - }), - { - templateDir: getSharedTemplateDir("vite-wv"), - }, - ); - plan.tasks.runInitialCodegen = true; - - await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "pnpm", tracker }))); - - expect(tracker.codegenTokens).toEqual(["initial-codegen-token"]); - - const { typegenConfig } = await readScaffoldArtifacts(path.join(cwd, "single-local-mcp-app")); - expect(typegenConfig).toContain('"connectedFileName": "OnlyOpen.fmp12"'); - }); - - it("persists detected local MCP file into typegen config when webviewer setup skips filemaker bootstrap", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-local-mcp-skip-bootstrap-")); - - const plan = planInit( - makeInitRequest({ - projectName: "skip-bootstrap-local-mcp-app", - scopedAppName: "skip-bootstrap-local-mcp-app", - appDir: "skip-bootstrap-local-mcp-app", - appType: "webviewer", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("vite-wv"), - }, - ); - - await Effect.runPromise( - executeInitPlan(plan).pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - fileMaker: { - localFmMcp: { - healthy: true, - baseUrl: "http://127.0.0.1:1365", - connectedFiles: ["Autodetected.fmp12"], - }, - }, - }), - ), - ); - - const { typegenConfig } = await readScaffoldArtifacts(path.join(cwd, "skip-bootstrap-local-mcp-app")); - expect(typegenConfig).toContain('"connectedFileName": "Autodetected.fmp12"'); - }); - - it("fails with a typed directory conflict in non-interactive mode", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-conflict-")); - const projectDir = path.join(cwd, "conflict-app"); - await fs.ensureDir(projectDir); - await fs.writeFile(path.join(projectDir, "README.md"), "existing"); - - const plan = planInit( - makeInitRequest({ - projectName: "conflict-app", - scopedAppName: "conflict-app", - appDir: "conflict-app", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - expect(await getFailure(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "pnpm" })))).toMatchObject( - new DirectoryConflictError({ - message: - "conflict-app already exists and isn't empty. Remove the existing files or choose a different directory.", - path: projectDir, - }), - ); - }); - - it("fails with a typed cancelation error when overwrite is aborted", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-abort-")); - const projectDir = path.join(cwd, "abort-app"); - await fs.ensureDir(projectDir); - await fs.writeFile(path.join(projectDir, "README.md"), "existing"); - - const plan = planInit( - makeInitRequest({ - projectName: "abort-app", - scopedAppName: "abort-app", - appDir: "abort-app", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: false, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - expect( - await getFailure( - executeInitPlan(plan).pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - nonInteractive: false, - prompts: { - select: ["abort"], - }, - }), - ), - ), - ).toMatchObject( - new UserCancelledError({ - message: "User aborted the operation", - }), - ); - }); - - it("fails with a typed directory conflict when overwrite prompt is cancelled", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-overwrite-cancel-")); - const projectDir = path.join(cwd, "cancel-app"); - await fs.ensureDir(projectDir); - await fs.writeFile(path.join(projectDir, "README.md"), "existing"); - - const plan = planInit( - makeInitRequest({ - projectName: "cancel-app", - scopedAppName: "cancel-app", - appDir: "cancel-app", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: false, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - expect( - await getFailure( - executeInitPlan(plan).pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - nonInteractive: false, - prompts: { - select: ["__cancel_value__"], - }, - }), - ), - ), - ).toMatchObject( - new DirectoryConflictError({ - message: "Unable to choose how to handle the existing directory.", - path: projectDir, - }), - ); - }); - - it("fails with a typed directory conflict when clear confirmation is cancelled", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-clear-cancel-")); - const projectDir = path.join(cwd, "clear-cancel-app"); - const existingFile = path.join(projectDir, "README.md"); - await fs.ensureDir(projectDir); - await fs.writeFile(existingFile, "existing"); - - const plan = planInit( - makeInitRequest({ - projectName: "clear-cancel-app", - scopedAppName: "clear-cancel-app", - appDir: "clear-cancel-app", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: false, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - expect( - await getFailure( - executeInitPlan(plan).pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - nonInteractive: false, - prompts: { - select: ["clear"], - confirm: ["__cancel_value__"], - }, - }), - ), - ), - ).toMatchObject( - new DirectoryConflictError({ - message: "Unable to confirm directory clearing.", - path: projectDir, - }), - ); - await expect(fs.pathExists(existingFile)).resolves.toBe(true); - }); - - it("prints pnpm warning in npm next steps", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-npm-warning-")); - const console = { - info: [] as string[], - warn: [] as string[], - error: [] as string[], - success: [] as string[], - note: [] as Array<{ message: string; title?: string }>, - }; - const plan = planInit( - makeInitRequest({ - projectName: "npm-app", - scopedAppName: "npm-app", - appDir: "npm-app", - packageManager: "npm", - noInstall: true, - noGit: true, - cwd, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - packageManagerVersion: "10.0.0", - }, - ); - - await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "npm", console }))); - - expect(console.info.join("\n")).toContain( - "Warning: We strongly suggest using PNPM 11 or greater as your package manager to better protect your computer and your app.", - ); - }); - - it("prints package manager execute command in agent setup next steps", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-pnpm-agent-setup-")); - const console = { - info: [] as string[], - warn: [] as string[], - error: [] as string[], - success: [] as string[], - note: [] as Array<{ message: string; title?: string }>, - }; - const plan = planInit( - makeInitRequest({ - projectName: "pnpm-app", - scopedAppName: "pnpm-app", - appDir: "pnpm-app", - packageManager: "pnpm", - noInstall: true, - noGit: true, - cwd, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - packageManagerVersion: "11.0.0", - }, - ); - - await Effect.runPromise(executeInitPlan(plan).pipe(makeTestLayer({ cwd, packageManager: "pnpm", console }))); - - expect(console.info.join("\n")).not.toContain("pnpx @tanstack/intent@latest install"); - }); - - it("fails with a typed external command error when install fails", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-install-fail-")); - const consoleTranscript = { - info: [] as string[], - warn: [] as string[], - error: [] as string[], - success: [] as string[], - note: [] as Array<{ message: string; title?: string }>, - }; - const plan = planInit( - makeInitRequest({ - projectName: "install-fail", - scopedAppName: "install-fail", - appDir: "install-fail", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "npm", - noInstall: false, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - expect( - await getFailure( - executeInitPlan(plan).pipe( - makeTestLayer({ - cwd, - packageManager: "npm", - console: consoleTranscript, - failures: { - processRun: new ExternalCommandError({ - message: "install failed", - command: "npm", - args: ["install"], - cwd, - }), - }, - }), - ), - ), - ).toMatchObject( - new ExternalCommandError({ - message: "install failed", - command: "npm", - args: ["install"], - cwd, - }), - ); - const installError = consoleTranscript.error.at(-1) ?? ""; - expect(installError).toContain("Install failed."); - expect(installError).toContain("Project root:"); - expect(installError).toContain("Failed command:"); - expect(installError).toContain("npm install"); - expect(installError).toContain("Succeeded before failure:"); - expect(installError).toContain("scaffold files"); - expect(installError).toContain("Continue troubleshooting:"); - expect(installError).toContain("cd install-fail"); - expect(installError).toContain("Start over:"); - }); - - it("fails with a typed codegen error when initial codegen fails", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-codegen-fail-")); - const plan = planInit( - makeInitRequest({ - projectName: "codegen-fail", - scopedAppName: "codegen-fail", - appDir: "codegen-fail", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - plan.tasks.runInitialCodegen = true; - - expect( - await getFailure( - executeInitPlan(plan).pipe( - makeTestLayer({ - cwd, - packageManager: "pnpm", - failures: { - codegenRun: new ExternalCommandError({ - message: "Initial codegen failed", - command: "pnpm", - args: ["typegen"], - cwd: path.join(cwd, "codegen-fail"), - }), - }, - }), - ), - ), - ).toMatchObject( - new ExternalCommandError({ - message: "Initial codegen failed", - command: "pnpm", - args: ["typegen"], - cwd: path.join(cwd, "codegen-fail"), - }), - ); - }); -}); diff --git a/packages/cli/tests/init-fixtures.ts b/packages/cli/tests/init-fixtures.ts deleted file mode 100644 index e7ee6afd..00000000 --- a/packages/cli/tests/init-fixtures.ts +++ /dev/null @@ -1,65 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import fs from "fs-extra"; -import type { InitRequest } from "~/core/types.js"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -export function makeInitRequest(overrides: Partial = {}): InitRequest { - return { - projectName: "demo-app", - scopedAppName: "demo-app", - appDir: "demo-app", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: false, - noGit: false, - force: false, - cwd: "/tmp/workspace", - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - ...overrides, - }; -} - -export function getSharedTemplateDir(templateName: "nextjs-shadcn" | "vite-wv") { - return path.resolve(__dirname, `../template/${templateName}`); -} - -export async function readScaffoldArtifacts(projectDir: string) { - const packageJson = await fs.readJson(path.join(projectDir, "package.json")); - const proofkitJson = await fs.readJson(path.join(projectDir, "proofkit.json")); - const envFile = await fs.readFile(path.join(projectDir, ".env"), "utf8"); - const typegenPath = path.join(projectDir, "proofkit-typegen.config.jsonc"); - const typegenConfig = (await fs.pathExists(typegenPath)) ? await fs.readFile(typegenPath, "utf8") : undefined; - const agentsPath = path.join(projectDir, "AGENTS.md"); - const claudePath = path.join(projectDir, "CLAUDE.md"); - const cursorIgnorePath = path.join(projectDir, ".cursorignore"); - const launchPath = path.join(projectDir, ".claude", "launch.json"); - const pnpmWorkspacePath = path.join(projectDir, "pnpm-workspace.yaml"); - const npmrcPath = path.join(projectDir, ".npmrc"); - const huskyPreCommitPath = path.join(projectDir, ".husky", "pre-commit"); - - return { - packageJson, - proofkitJson, - envFile, - typegenConfig, - agentsFile: (await fs.pathExists(agentsPath)) ? await fs.readFile(agentsPath, "utf8") : undefined, - claudeFile: (await fs.pathExists(claudePath)) ? await fs.readFile(claudePath, "utf8") : undefined, - cursorIgnoreFile: (await fs.pathExists(cursorIgnorePath)) ? await fs.readFile(cursorIgnorePath, "utf8") : undefined, - launchConfig: (await fs.pathExists(launchPath)) ? await fs.readFile(launchPath, "utf8") : undefined, - pnpmWorkspaceFile: (await fs.pathExists(pnpmWorkspacePath)) - ? await fs.readFile(pnpmWorkspacePath, "utf8") - : undefined, - npmrcFile: (await fs.pathExists(npmrcPath)) ? await fs.readFile(npmrcPath, "utf8") : undefined, - huskyPreCommitFile: (await fs.pathExists(huskyPreCommitPath)) - ? await fs.readFile(huskyPreCommitPath, "utf8") - : undefined, - }; -} diff --git a/packages/cli/tests/init-non-interactive-failures.test.ts b/packages/cli/tests/init-non-interactive-failures.test.ts deleted file mode 100644 index 6c52ceb0..00000000 --- a/packages/cli/tests/init-non-interactive-failures.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import { beforeEach, describe, expect, it } from "vitest"; -import { TYPEGEN_VERSION } from "../src/package-versions.js"; - -type ExecFailure = Error & { - status?: number | null; - stdout?: string | Buffer; - stderr?: string | Buffer; -}; -const typegenCommandPattern = /\b(?:npm run|pnpm|yarn|bun)\s+typegen\b/; - -function toText(value: string | Buffer | undefined) { - if (typeof value === "string") { - return value; - } - if (!value) { - return ""; - } - return value.toString("utf-8"); -} - -describe("Init Non-Interactive Failure Paths", () => { - const testDir = join(import.meta.dirname, "..", "..", "tmp", "init-failure-tests"); - const cliPath = join(import.meta.dirname, "..", "bin", "proofkit.cjs"); - const expectedTypegenVersion = `^${TYPEGEN_VERSION}`; - - beforeEach(() => { - rmSync(testDir, { recursive: true, force: true }); - mkdirSync(testDir, { recursive: true }); - }); - - const runInitCommand = (args: string[], cwd = testDir) => { - return execFileSync("node", [cliPath, "init", ...args], { - cwd, - env: process.env, - stdio: "pipe", - encoding: "utf-8", - }); - }; - - const runInitExpectFailure = (args: string[], cwd = testDir) => { - try { - runInitCommand(args, cwd); - throw new Error(`Expected init to fail, but it succeeded: ${args.join(" ")}`); - } catch (error) { - const failure = error as ExecFailure; - if (typeof failure.status === "number" || failure.status === null) { - return { - status: failure.status, - stdout: toText(failure.stdout), - stderr: toText(failure.stderr), - }; - } - throw error; - } - }; - - const runInitExpectSuccess = (args: string[], cwd = testDir) => runInitCommand(args, cwd); - - it("fails in non-interactive mode without a project name and does not scaffold", () => { - writeFileSync(join(testDir, "sentinel.txt"), "keep"); - - const result = runInitExpectFailure(["--non-interactive", "--app-type", "webviewer", "--no-install", "--no-git"]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("Project name is required in non-interactive mode."); - expect(readdirSync(testDir).sort()).toEqual(["sentinel.txt"]); - }); - - it("normalizes spaces in non-interactive app names and creates the project directory", () => { - const projectName = "Bad Name"; - - runInitExpectSuccess([projectName, "--non-interactive", "--app-type", "webviewer", "--no-install", "--no-git"]); - - expect(existsSync(join(testDir, "bad-name"))).toBe(true); - expect(existsSync(join(testDir, projectName))).toBe(false); - }); - - it("fails for invalid scoped-path edge cases before mutating the target directory", () => { - writeFileSync(join(testDir, "README.md"), "existing content"); - - const result = runInitExpectFailure([ - "@scope", - "--non-interactive", - "--app-type", - "webviewer", - "--no-install", - "--no-git", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("Name must consist of only lowercase alphanumeric characters, '-', and '_'"); - expect(readFileSync(join(testDir, "README.md"), "utf-8")).toBe("existing content"); - expect(existsSync(join(testDir, "package.json"))).toBe(false); - expect(existsSync(join(testDir, "proofkit.json"))).toBe(false); - }); - - it("fails for partial FileMaker schema flags without creating a scaffold", () => { - const projectName = "partial-filemaker-flags"; - - const result = runInitExpectFailure([ - projectName, - "--non-interactive", - "--app-type", - "webviewer", - "--data-source", - "filemaker", - "--layout-name", - "Contacts", - "--no-install", - "--no-git", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("Both --layout-name and --schema-name must be provided together."); - expect(existsSync(join(testDir, projectName))).toBe(false); - }); - - it("fails when FileMaker flags are passed without selecting the filemaker data source", () => { - const projectName = "unsupported-filemaker-flags"; - - const result = runInitExpectFailure([ - projectName, - "--non-interactive", - "--app-type", - "webviewer", - "--layout-name", - "Contacts", - "--schema-name", - "Contacts", - "--no-install", - "--no-git", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("FileMaker flags require --data-source filemaker in non-interactive mode."); - expect(existsSync(join(testDir, projectName))).toBe(false); - }); - - it("preserves existing directory contents when validation fails even with --force", () => { - const projectName = "force-validation-failure"; - const projectDir = join(testDir, projectName); - mkdirSync(projectDir, { recursive: true }); - writeFileSync(join(projectDir, "README.md"), "existing content"); - - const result = runInitExpectFailure([ - projectName, - "--non-interactive", - "--app-type", - "webviewer", - "--force", - "--layout-name", - "Contacts", - "--schema-name", - "Contacts", - "--no-install", - "--no-git", - ]); - const output = `${result.stdout}\n${result.stderr}`; - - expect(result.status).toBe(1); - expect(output).toContain("FileMaker flags require --data-source filemaker in non-interactive mode."); - expect(readFileSync(join(projectDir, "README.md"), "utf-8")).toBe("existing content"); - expect(existsSync(join(projectDir, "package.json"))).toBe(false); - expect(existsSync(join(projectDir, "proofkit.json"))).toBe(false); - }); - - it("adds package-native typegen guidance for browser scaffolds", () => { - const projectName = "browser-no-fm-guidance"; - const output = runInitExpectSuccess([ - projectName, - "--non-interactive", - "--app-type", - "browser", - "--data-source", - "none", - "--no-install", - "--no-git", - ]); - - const packageJson = JSON.parse(readFileSync(join(testDir, projectName, "package.json"), "utf-8")) as { - scripts?: Record; - devDependencies?: Record; - }; - expect(packageJson.scripts?.typegen).toBe("typegen"); - expect(packageJson.devDependencies?.["@proofkit/typegen"]).toBe(expectedTypegenVersion); - expect(output).not.toMatch(typegenCommandPattern); - }); -}); diff --git a/packages/cli/tests/init-post-init-generation-errors.test.ts b/packages/cli/tests/init-post-init-generation-errors.test.ts deleted file mode 100644 index 0a40fd41..00000000 --- a/packages/cli/tests/init-post-init-generation-errors.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import { createPostInitGenerationError, isMissingTypegenCommandError } from "~/cli/init.js"; - -vi.mock("@inquirer/prompts", () => ({ - checkbox: vi.fn(), - confirm: vi.fn(), - input: vi.fn(), - password: vi.fn(), - search: vi.fn(), - select: vi.fn(), -})); - -describe("init post-init generation error handling", () => { - it("detects missing typegen command failures", () => { - const commandError = new Error( - 'Command failed with exit code 254: pnpm typegen\nERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "typegen" not found', - ); - - expect(isMissingTypegenCommandError(commandError)).toBe(true); - }); - - it("does not classify broad pnpm typegen execution failures as missing command", () => { - const commandError = new Error( - "Command failed with exit code 1: pnpm typegen\nError: connect ECONNREFUSED 127.0.0.1:3000", - ); - - expect(isMissingTypegenCommandError(commandError)).toBe(false); - }); - - it("creates browser-specific guidance for missing typegen command failures", () => { - const commandError = new Error( - 'Command failed with exit code 254: pnpm typegen\nERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "typegen" not found', - ); - - const userFacingError = createPostInitGenerationError({ - error: commandError, - appType: "browser", - projectDir: "/tmp/demo-browser", - }); - - expect(userFacingError.message).toContain("Post-init generation failed after scaffolding."); - expect(userFacingError.message).toContain("Project created at: /tmp/demo-browser"); - expect(userFacingError.message).toContain("browser scaffolds do not define that script"); - expect(userFacingError.message).toContain("npx @proofkit/typegen"); - }); - - it("creates generic recovery guidance for other generation failures", () => { - const commandError = new Error("Unable to read layout metadata"); - - const userFacingError = createPostInitGenerationError({ - error: commandError, - appType: "webviewer", - projectDir: "/tmp/demo-webviewer", - }); - - expect(userFacingError.message).toContain("Post-init generation failed after scaffolding."); - expect(userFacingError.message).toContain("Project created at: /tmp/demo-webviewer"); - expect(userFacingError.message).toContain("Retry `npx @proofkit/typegen`"); - expect(userFacingError.message).toContain("Underlying error: Unable to read layout metadata"); - }); -}); diff --git a/packages/cli/tests/init-run-init-regression.test.ts b/packages/cli/tests/init-run-init-regression.test.ts deleted file mode 100644 index 09502384..00000000 --- a/packages/cli/tests/init-run-init-regression.test.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const { - createBareProjectMock, - setImportAliasMock, - promptForFileMakerDataSourceMock, - runCodegenCommandMock, - initializeGitMock, - logNextStepsMock, - readJSONSyncMock, - writeJSONSyncMock, - writeFileSyncMock, - execaMock, - mockState, -} = vi.hoisted(() => ({ - createBareProjectMock: vi.fn(), - setImportAliasMock: vi.fn(), - promptForFileMakerDataSourceMock: vi.fn(), - runCodegenCommandMock: vi.fn(), - initializeGitMock: vi.fn(), - logNextStepsMock: vi.fn(), - readJSONSyncMock: vi.fn(), - writeJSONSyncMock: vi.fn(), - writeFileSyncMock: vi.fn(), - execaMock: vi.fn(), - mockState: { - appType: undefined as "browser" | "webviewer" | undefined, - ui: "shadcn" as const, - projectDir: "/tmp/proofkit-regression", - }, -})); - -vi.mock("@clack/prompts", () => ({ - intro: vi.fn(), - outro: vi.fn(), - note: vi.fn(), - cancel: vi.fn(), - log: { - error: vi.fn(), - info: vi.fn(), - message: vi.fn(), - step: vi.fn(), - success: vi.fn(), - warn: vi.fn(), - }, - spinner: vi.fn(() => ({ - message: vi.fn(), - start: vi.fn(), - stop: vi.fn(), - })), - isCancel: vi.fn(() => false), - select: vi.fn(), - text: vi.fn(), -})); - -vi.mock("@inquirer/prompts", () => ({ - checkbox: vi.fn(), - confirm: vi.fn(), - input: vi.fn(), - password: vi.fn(), - search: vi.fn(), - select: vi.fn(), -})); - -vi.mock("fs-extra", () => ({ - default: { - readJSONSync: readJSONSyncMock, - writeJSONSync: writeJSONSyncMock, - writeFileSync: writeFileSyncMock, - }, -})); - -vi.mock("execa", () => ({ - execa: execaMock, -})); - -vi.mock("~/helpers/createProject.js", () => ({ - createBareProject: createBareProjectMock, -})); - -vi.mock("~/helpers/setImportAlias.js", () => ({ - setImportAlias: setImportAliasMock, -})); - -vi.mock("~/cli/add/data-source/filemaker.js", () => ({ - promptForFileMakerDataSource: promptForFileMakerDataSourceMock, -})); - -vi.mock("~/generators/fmdapi.js", () => ({ - runCodegenCommand: runCodegenCommandMock, -})); - -vi.mock("~/helpers/git.js", () => ({ - initializeGit: initializeGitMock, -})); - -vi.mock("~/helpers/logNextSteps.js", () => ({ - logNextSteps: logNextStepsMock, -})); - -vi.mock("~/helpers/installDependencies.js", () => ({ - installDependencies: vi.fn(), -})); - -vi.mock("~/generators/auth.js", () => ({ - addAuth: vi.fn(), -})); - -vi.mock("~/installers/index.js", () => ({ - buildPkgInstallerMap: vi.fn(() => ({})), -})); - -vi.mock("~/state.js", () => ({ - state: mockState, - initProgramState: vi.fn(), - isNonInteractiveMode: vi.fn(() => true), -})); - -vi.mock("~/utils/getProofKitVersion.js", () => ({ - getVersion: vi.fn(() => "0.0.0-test"), -})); - -vi.mock("~/utils/getUserPkgManager.js", () => ({ - getUserPkgManager: vi.fn(() => "pnpm"), -})); - -vi.mock("~/utils/parseNameAndPath.js", () => ({ - parseNameAndPath: vi.fn((name: string) => [name, name]), -})); - -vi.mock("~/utils/parseSettings.js", () => ({ - setSettings: vi.fn(), -})); - -vi.mock("~/utils/validateAppName.js", () => ({ - validateAppName: vi.fn(() => undefined), -})); - -vi.mock("~/cli/utils.js", () => ({ - abortIfCancel: vi.fn((value: unknown) => value), -})); - -import { runInit } from "~/cli/init.js"; - -const browserFilemakerFlags = { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - server: undefined, - adminApiKey: undefined, - fileName: "", - layoutName: "", - schemaName: "", - dataApiKey: "", - fmServerURL: "", - auth: "none" as const, - dataSource: "filemaker" as const, - ui: "shadcn" as const, - CI: false, - nonInteractive: true, - tailwind: false, - trpc: false, - prisma: false, - drizzle: false, - appRouter: false, -}; - -describe("runInit browser post-init typegen regression", () => { - beforeEach(() => { - vi.clearAllMocks(); - - mockState.appType = undefined; - mockState.ui = "shadcn"; - mockState.projectDir = "/tmp/proofkit-regression"; - - createBareProjectMock.mockResolvedValue("/tmp/proofkit-regression/demo-browser"); - readJSONSyncMock.mockReturnValue({ name: "placeholder-app" }); - execaMock.mockResolvedValue({ stdout: "9.0.0" }); - promptForFileMakerDataSourceMock.mockResolvedValue(undefined); - - runCodegenCommandMock.mockRejectedValue( - new Error( - 'Command failed with exit code 254: pnpm typegen\nERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "typegen" not found', - ), - ); - }); - - it("does not run initial codegen for browser scaffolds after filemaker setup", async () => { - await expect(runInit("demo-browser", browserFilemakerFlags)).resolves.toBeUndefined(); - - expect(promptForFileMakerDataSourceMock).toHaveBeenCalledWith( - expect.objectContaining({ - projectDir: "/tmp/proofkit-regression/demo-browser", - }), - ); - expect(runCodegenCommandMock).not.toHaveBeenCalled(); - }); - - it("writes pnpm build policy before install for pnpm 10", async () => { - mockState.appType = "webviewer"; - execaMock.mockResolvedValue({ stdout: "10.0.0" }); - - await expect( - runInit("demo-webviewer", { - ...browserFilemakerFlags, - noInstall: false, - dataSource: "none", - }), - ).resolves.toBeUndefined(); - - expect(writeFileSyncMock).toHaveBeenCalledWith( - "/tmp/proofkit-regression/demo-browser/pnpm-workspace.yaml", - expect.stringContaining(' "sharp": false'), - "utf8", - ); - const workspaceWriteCallIndex = writeFileSyncMock.mock.calls.findIndex(([filePath]) => - String(filePath).endsWith("pnpm-workspace.yaml"), - ); - const firstPnpmScriptCallIndex = execaMock.mock.calls.findIndex( - ([command, args]) => command === "pnpm" && Array.isArray(args) && args[0] !== "-v", - ); - expect(workspaceWriteCallIndex).not.toBe(-1); - expect(firstPnpmScriptCallIndex).not.toBe(-1); - const workspaceWriteOrder = writeFileSyncMock.mock.invocationCallOrder[workspaceWriteCallIndex]; - const firstPnpmScriptOrder = execaMock.mock.invocationCallOrder[firstPnpmScriptCallIndex]; - expect(workspaceWriteOrder).toBeDefined(); - expect(firstPnpmScriptOrder).toBeDefined(); - expect(workspaceWriteOrder as number).toBeLessThan(firstPnpmScriptOrder as number); - expect(execaMock).toHaveBeenCalledWith("pnpm", ["fix"], { - cwd: "/tmp/proofkit-regression/demo-browser", - stdio: "pipe", - }); - expect(execaMock).toHaveBeenCalledWith("pnpm", ["lint"], { - cwd: "/tmp/proofkit-regression/demo-browser", - stdio: "pipe", - }); - }); -}); diff --git a/packages/cli/tests/init-scaffold-contract.test.ts b/packages/cli/tests/init-scaffold-contract.test.ts deleted file mode 100644 index 34f394bd..00000000 --- a/packages/cli/tests/init-scaffold-contract.test.ts +++ /dev/null @@ -1,309 +0,0 @@ -import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:fs"; -import { join } from "node:path"; -import { parse as parseJsonc } from "jsonc-parser/lib/esm/main.js"; -import { beforeEach, describe, expect, it } from "vitest"; -import { NODE_RUNTIME_VERSION } from "~/consts.js"; -import { FMDAPI_VERSION, TYPEGEN_VERSION, WEBVIEWER_VERSION } from "../src/package-versions.js"; - -interface PackageJsonShape { - version?: string; - name?: string; - packageManager?: string; - devEngines?: { - packageManager?: { - name?: string; - version?: string; - onFail?: string; - }; - runtime?: { - name?: string; - version?: string; - onFail?: string; - }; - }; - engines?: { - node?: string; - }; - scripts?: Record; - "lint-staged"?: Record; - dependencies?: Record; - devDependencies?: Record; - proofkitMetadata?: { - initVersion?: string; - }; -} - -interface ProofkitSettings { - appType?: string; - ui?: string; - envFile?: string; - dataSources?: unknown[]; -} - -const cliPath = join(import.meta.dirname, "..", "bin", "proofkit.cjs"); -const testDir = join(import.meta.dirname, "..", "..", "tmp", "cli-contract-tests"); -const browserProjectName = "contract-browser-project"; -const webviewerProjectName = "contract-webviewer-project"; -const browserProjectDir = join(testDir, browserProjectName); -const webviewerProjectDir = join(testDir, webviewerProjectName); -const cliPackageJsonPath = join(import.meta.dirname, "..", "package.json"); -const cliPackageJson = readJsonFile(cliPackageJsonPath); -const cliVersion = cliPackageJson.version ?? ""; -const expectedProofkitVersions = new Map([ - ["@proofkit/fmdapi", `^${FMDAPI_VERSION}`], - ["@proofkit/typegen", `^${TYPEGEN_VERSION}`], - ["@proofkit/webviewer", `^${WEBVIEWER_VERSION}`], -]); -const packageManagerVersionPattern = /^\^\d+\.\d+\.\d+/; -const ansiStylePrefixPattern = /^[0-9;]*m/; - -function runInit({ appType, projectName }: { appType: "browser" | "webviewer"; projectName: string }): string { - return execFileSync( - "node", - [ - cliPath, - "init", - projectName, - "--non-interactive", - "--app-type", - appType, - "--data-source", - "none", - "--no-git", - "--no-install", - ], - { - cwd: testDir, - env: process.env, - encoding: "utf-8", - stdio: "pipe", - }, - ); -} - -function readJsonFile(filePath: string): T { - return JSON.parse(readFileSync(filePath, "utf-8")) as T; -} - -function getProofkitDependencyVersions(pkg: PackageJsonShape): [string, string][] { - const combined = { - ...(pkg.dependencies ?? {}), - ...(pkg.devDependencies ?? {}), - }; - - return Object.entries(combined) - .filter(([name]) => name.startsWith("@proofkit/")) - .map(([name, version]) => [name, version]); -} - -function allProofkitDependenciesUseCurrentVersions(pkg: PackageJsonShape): boolean { - const versions = getProofkitDependencyVersions(pkg); - return versions.length > 0 && versions.every(([name, version]) => version === expectedProofkitVersions.get(name)); -} - -function checkNodeSyntax(projectDir: string, relativeFilePath: string): boolean { - try { - execFileSync("node", ["--check", relativeFilePath], { - cwd: projectDir, - env: process.env, - encoding: "utf-8", - stdio: "pipe", - }); - - return true; - } catch { - return false; - } -} - -function getPackageManagerName(packageJson: PackageJsonShape): "npm" | "pnpm" | "yarn" | "bun" { - const raw = packageJson.devEngines?.packageManager?.name ?? packageJson.packageManager?.split("@")[0]; - if (raw === "pnpm" || raw === "yarn" || raw === "bun") { - return raw; - } - return "npm"; -} - -function formatRunCommand(pkgManager: "npm" | "pnpm" | "yarn" | "bun", command: string): string { - return pkgManager === "npm" || pkgManager === "bun" ? `${pkgManager} run ${command}` : `${pkgManager} ${command}`; -} - -function sanitizeOutput(output: string): string { - return output - .split("\u001b[") - .map((segment, index) => (index === 0 ? segment : segment.replace(ansiStylePrefixPattern, ""))) - .join(""); -} - -function outputSuggestsCommand(output: string, command: string): boolean { - return output.includes(` ${command}`); -} - -describe("Init scaffold contract tests", () => { - beforeEach(() => { - rmSync(testDir, { recursive: true, force: true }); - mkdirSync(testDir, { recursive: true }); - }); - - it("creates deterministic browser scaffold output in non-interactive mode", () => { - const initOutput = runInit({ - appType: "browser", - projectName: browserProjectName, - }); - const normalizedOutput = sanitizeOutput(initOutput); - - expect(existsSync(browserProjectDir)).toBe(true); - expect(existsSync(join(browserProjectDir, "package.json"))).toBe(true); - expect(existsSync(join(browserProjectDir, "proofkit.json"))).toBe(true); - expect(existsSync(join(browserProjectDir, ".env"))).toBe(true); - expect(existsSync(join(browserProjectDir, ".cursorignore"))).toBe(true); - expect(existsSync(join(browserProjectDir, "pnpm-workspace.yaml"))).toBe(true); - expect(existsSync(join(browserProjectDir, "src", "lib", "env.ts"))).toBe(true); - expect(existsSync(join(browserProjectDir, "src", "app", "layout.tsx"))).toBe(true); - expect(existsSync(join(browserProjectDir, "postcss.config.mjs"))).toBe(true); - - const packageJson = readJsonFile(join(browserProjectDir, "package.json")); - expect(packageJson.name).toBe(browserProjectName); - expect(packageJson.scripts?.dev).toBe("next dev --turbopack"); - expect(packageJson.scripts?.build).toBe("next build --turbopack"); - expect(packageJson.scripts?.proofkit).toBeUndefined(); - expect(packageJson.proofkitMetadata?.initVersion).toBe(cliVersion); - expect(packageJson.packageManager).toBeUndefined(); - expect(packageJson.devEngines?.packageManager?.name).toBe("pnpm"); - expect(packageJson.devEngines?.packageManager?.version).toMatch(packageManagerVersionPattern); - expect(packageJson.devEngines?.packageManager?.onFail).toBe("download"); - expect(packageJson.devEngines?.runtime).toEqual({ - name: "node", - version: NODE_RUNTIME_VERSION, - onFail: "download", - }); - expect(packageJson.engines?.node).toBe(NODE_RUNTIME_VERSION); - expect(allProofkitDependenciesUseCurrentVersions(packageJson)).toBe(true); - expect(readFileSync(join(browserProjectDir, "CLAUDE.md"), "utf-8")).toBe("@AGENTS.md\n"); - expect(readFileSync(join(browserProjectDir, ".cursorignore"), "utf-8")).toBe("CLAUDE.md\n"); - expect(readFileSync(join(browserProjectDir, ".gitignore"), "utf-8")).toContain(".pnpm-store"); - expect(readFileSync(join(browserProjectDir, ".husky", "pre-commit"), "utf-8")).toBe( - '#!/bin/sh\necho "Running lint-staged..."\npnpm exec lint-staged\n', - ); - const pnpmWorkspaceText = readFileSync(join(browserProjectDir, "pnpm-workspace.yaml"), "utf-8"); - expect(pnpmWorkspaceText).toContain(' "esbuild": true'); - expect(pnpmWorkspaceText).toContain(' "msw": true'); - expect(pnpmWorkspaceText).toContain(' "@parcel/watcher": true'); - expect(pnpmWorkspaceText).toContain(' "node": true'); - expect(pnpmWorkspaceText).toContain(' "sharp": true'); - expect(pnpmWorkspaceText).toContain(' "msgpackr-extract": true'); - expect(pnpmWorkspaceText).toContain("packages:"); - expect(pnpmWorkspaceText).toContain(' - "."'); - expect(pnpmWorkspaceText).toContain("trustPolicy: no-downgrade"); - expect(pnpmWorkspaceText).toContain("trustPolicyIgnoreAfter: 43200"); - expect(pnpmWorkspaceText).toContain("blockExoticSubdeps: true"); - const pkgManager = getPackageManagerName(packageJson); - expect(outputSuggestsCommand(normalizedOutput, formatRunCommand(pkgManager, "typegen"))).toBe(false); - - const proofkitConfig = readJsonFile(join(browserProjectDir, "proofkit.json")); - expect(proofkitConfig.appType).toBe("browser"); - expect(proofkitConfig.ui).toBe("shadcn"); - expect(proofkitConfig.envFile).toBe(".env"); - expect(proofkitConfig.dataSources).toEqual([]); - - // Compile-equivalent smoke check without external installs. - expect(checkNodeSyntax(browserProjectDir, "postcss.config.mjs")).toBe(true); - }); - - it("creates deterministic webviewer scaffold output in non-interactive mode", () => { - const initOutput = runInit({ - appType: "webviewer", - projectName: webviewerProjectName, - }); - const normalizedOutput = sanitizeOutput(initOutput); - - expect(existsSync(webviewerProjectDir)).toBe(true); - expect(existsSync(join(webviewerProjectDir, "package.json"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "proofkit.json"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "proofkit-typegen.config.jsonc"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, ".env"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, ".cursorignore"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "pnpm-workspace.yaml"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "src", "app.tsx"))).toBe(true); - expect(readdirSync(join(webviewerProjectDir, "src"))).not.toContain("App.tsx"); - expect(existsSync(join(webviewerProjectDir, "src", "main.tsx"))).toBe(true); - expect(existsSync(join(webviewerProjectDir, "scripts", "upload.js"))).toBe(true); - - const packageJson = readJsonFile(join(webviewerProjectDir, "package.json")); - expect(packageJson.name).toBe(webviewerProjectName); - expect(packageJson.scripts?.build).toBe("vite build"); - expect(packageJson.scripts?.typegen).toBe("pnpx @proofkit/typegen"); - expect(packageJson.scripts?.["typegen:ui"]).toBe("pnpx @proofkit/typegen ui"); - expect(packageJson.scripts?.check).toBe("ultracite check"); - expect(packageJson.scripts?.fix).toBe("ultracite fix"); - expect(packageJson.scripts?.proofkit).toBeUndefined(); - expect(packageJson["lint-staged"]).toEqual({ - "*.{js,jsx,ts,tsx,json,jsonc,css,scss,md,mdx}": ["pnpm exec ultracite fix"], - }); - expect(packageJson.proofkitMetadata?.initVersion).toBe(cliVersion); - expect(packageJson.packageManager).toBeUndefined(); - expect(packageJson.devEngines?.packageManager?.name).toBe("pnpm"); - expect(packageJson.devEngines?.packageManager?.version).toMatch(packageManagerVersionPattern); - expect(packageJson.devEngines?.packageManager?.onFail).toBe("download"); - expect(packageJson.devEngines?.runtime).toEqual({ - name: "node", - version: NODE_RUNTIME_VERSION, - onFail: "download", - }); - expect(packageJson.engines?.node).toBe(NODE_RUNTIME_VERSION); - expect(allProofkitDependenciesUseCurrentVersions(packageJson)).toBe(true); - expect(readFileSync(join(webviewerProjectDir, "CLAUDE.md"), "utf-8")).toBe("@AGENTS.md\n"); - expect(readFileSync(join(webviewerProjectDir, ".cursorignore"), "utf-8")).toBe("CLAUDE.md\n"); - expect(readFileSync(join(webviewerProjectDir, ".gitignore"), "utf-8")).toContain(".pnpm-store"); - expect(readFileSync(join(webviewerProjectDir, ".husky", "pre-commit"), "utf-8")).toBe( - '#!/bin/sh\necho "Running lint-staged..."\npnpm exec lint-staged\n', - ); - const pkgManager = getPackageManagerName(packageJson); - expect(outputSuggestsCommand(normalizedOutput, formatRunCommand(pkgManager, "typegen"))).toBe(true); - - const proofkitConfig = readJsonFile(join(webviewerProjectDir, "proofkit.json")); - expect(proofkitConfig.appType).toBe("webviewer"); - expect(proofkitConfig.ui).toBe("shadcn"); - expect(proofkitConfig.envFile).toBe(".env"); - expect(proofkitConfig.dataSources).toEqual([]); - - const typegenConfigText = readFileSync(join(webviewerProjectDir, "proofkit-typegen.config.jsonc"), "utf-8"); - const typegenConfig = parseJsonc(typegenConfigText) as { - config?: { - type?: string; - path?: string; - validator?: string; - webviewerScriptName?: string; - fmMcp?: { - enabled?: boolean; - }; - }; - }; - expect(typegenConfig.config?.type).toBe("fmdapi"); - expect(typegenConfig.config?.path).toBe("./src/config/schemas/filemaker"); - expect(typegenConfig.config?.validator).toBe("zod/v4"); - expect(typegenConfig.config?.webviewerScriptName).toBe("ExecuteDataApi"); - expect(typegenConfig.config?.fmMcp?.enabled).toBe(true); - - const uploadScriptText = readFileSync(join(webviewerProjectDir, "scripts", "upload.js"), "utf-8"); - const filemakerHelperText = readFileSync(join(webviewerProjectDir, "scripts", "filemaker.js"), "utf-8"); - const pnpmWorkspaceText = readFileSync(join(webviewerProjectDir, "pnpm-workspace.yaml"), "utf-8"); - expect(uploadScriptText).toContain("const deployment = await deployHtml({"); - expect(uploadScriptText).toContain("Deployed via FM MCP bridge."); - expect(filemakerHelperText).toContain('scriptName = "deploy_html"'); - expect(pnpmWorkspaceText).toContain(' "esbuild": true'); - expect(pnpmWorkspaceText).toContain(' "msw": true'); - expect(pnpmWorkspaceText).toContain(' "@parcel/watcher": true'); - expect(pnpmWorkspaceText).toContain(' "sharp": false'); - expect(pnpmWorkspaceText).toContain(' "msgpackr-extract": true'); - expect(pnpmWorkspaceText).toContain("packages:"); - expect(pnpmWorkspaceText).toContain(' - "."'); - expect(pnpmWorkspaceText).toContain("trustPolicy: no-downgrade"); - expect(pnpmWorkspaceText).toContain("trustPolicyIgnoreAfter: 43200"); - expect(pnpmWorkspaceText).toContain("blockExoticSubdeps: true"); - - // Compile-equivalent smoke checks without external installs. - expect(checkNodeSyntax(webviewerProjectDir, "scripts/upload.js")).toBe(true); - }); -}); diff --git a/packages/cli/tests/install-fm-addon.test.ts b/packages/cli/tests/install-fm-addon.test.ts deleted file mode 100644 index 0d0b83ec..00000000 --- a/packages/cli/tests/install-fm-addon.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import os from "node:os"; -import path from "node:path"; -import fs from "fs-extra"; -import { describe, expect, it } from "vitest"; -import { compareAddonVersions, getFmAddonInstallInstructions, inspectFmAddon } from "~/installers/install-fm-addon.js"; -import { getWebViewerAddonMessages } from "~/installers/proofkit-webviewer.js"; - -async function writeAddonVersion(dir: string, version: string) { - await fs.ensureDir(dir); - await fs.writeFile( - path.join(dir, "template.xml"), - ``, - "utf8", - ); -} - -describe("inspectFmAddon", () => { - it("returns unknown when the platform is unsupported", async () => { - const result = await inspectFmAddon( - { addonName: "wv" }, - { - targetDir: null, - latestAddonPath: "/tmp/latest-addon", - }, - ); - - expect(result.status).toBe("unknown"); - expect(result.reason).toBe("unsupported-platform"); - }); - - it("returns missing when the local add-on is absent", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-addon-missing-")); - const latestAddonPath = path.join(root, "latest", "ProofKitWV"); - const targetDir = path.join(root, "target"); - await writeAddonVersion(latestAddonPath, "2.2.3.0"); - await fs.ensureDir(targetDir); - - const result = await inspectFmAddon({ addonName: "wv" }, { targetDir, latestAddonPath }); - - expect(result.status).toBe("missing"); - expect(result.latestVersion).toBe("2.2.3.0"); - }); - - it("returns installed-current when versions match", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-addon-current-")); - const latestAddonPath = path.join(root, "latest", "ProofKitWV"); - const targetDir = path.join(root, "target"); - await writeAddonVersion(latestAddonPath, "2.2.3.0"); - await writeAddonVersion(path.join(targetDir, "ProofKitWV"), "2.2.3.0"); - - const result = await inspectFmAddon({ addonName: "wv" }, { targetDir, latestAddonPath }); - - expect(result.status).toBe("installed-current"); - expect(result.installedVersion).toBe("2.2.3.0"); - }); - - it("returns installed-outdated when the remote add-on is newer", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-addon-outdated-")); - const latestAddonPath = path.join(root, "latest", "ProofKitWV"); - const targetDir = path.join(root, "target"); - await writeAddonVersion(latestAddonPath, "2.2.4.0"); - await writeAddonVersion(path.join(targetDir, "ProofKitWV"), "2.2.3.0"); - - const result = await inspectFmAddon({ addonName: "wv" }, { targetDir, latestAddonPath }); - - expect(result.status).toBe("installed-outdated"); - expect(result.installedVersion).toBe("2.2.3.0"); - expect(result.latestVersion).toBe("2.2.4.0"); - }); - - it("returns unknown when installed metadata cannot be parsed", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-addon-unknown-")); - const latestAddonPath = path.join(root, "latest", "ProofKitWV"); - const targetDir = path.join(root, "target"); - await writeAddonVersion(latestAddonPath, "2.2.4.0"); - await fs.ensureDir(path.join(targetDir, "ProofKitWV")); - await fs.writeFile(path.join(targetDir, "ProofKitWV", "template.xml"), "", "utf8"); - - const result = await inspectFmAddon({ addonName: "wv" }, { targetDir, latestAddonPath }); - - expect(result.status).toBe("unknown"); - expect(result.reason).toBe("unreadable-version"); - }); -}); - -describe("compareAddonVersions", () => { - it("preserves the fourth version segment", () => { - expect(compareAddonVersions("2.2.3.0", "2.2.3.1")).toBe(-1); - expect(compareAddonVersions("2.2.3.1", "2.2.3.0")).toBe(1); - }); -}); - -describe("getWebViewerAddonMessages", () => { - it("adds an explicit update command when the local add-on is outdated", () => { - const messages = getWebViewerAddonMessages({ - hasRequiredLayouts: true, - inspection: { - status: "installed-outdated", - addonName: "wv", - addonDir: "ProofKitWV", - addonDisplayName: "ProofKit Web Viewer", - installCommand: "proofkit add addon webviewer", - targetDir: "/tmp/ProofKit", - installedPath: "/tmp/ProofKit/ProofKit.fmaddon", - installedVersion: "2.2.3.0", - remoteAssetUrl: "https://downloads.ottomatic.cloud/proofkit/addons/ProofKitWV.fmaddon", - latestVersion: "2.2.4.0", - }, - }); - - expect(messages.warn.join("\n")).toContain("proofkit add addon webviewer"); - expect(messages.nextSteps).toEqual(["proofkit add addon webviewer"]); - }); -}); - -describe("getFmAddonInstallInstructions", () => { - it("includes direct-open install guidance", () => { - const instructions = getFmAddonInstallInstructions("wv"); - - expect(instructions.steps).toContain("When FileMaker opens the add-on file, confirm the install prompt"); - expect(instructions.steps.join("\n")).not.toContain("Restart FileMaker"); - }); -}); diff --git a/packages/cli/tests/integration.test.ts b/packages/cli/tests/integration.test.ts deleted file mode 100644 index 36df5e88..00000000 --- a/packages/cli/tests/integration.test.ts +++ /dev/null @@ -1,315 +0,0 @@ -import os from "node:os"; -import path from "node:path"; -import { Effect } from "effect"; -import fs from "fs-extra"; -import { describe, expect, it } from "vitest"; -import { NODE_RUNTIME_VERSION } from "~/consts.js"; -import { executeInitPlan } from "~/core/executeInitPlan.js"; -import { planInit } from "~/core/planInit.js"; -import { getProofkitDependencyVersion, getTypegenVersion } from "~/utils/getProofKitVersion.js"; -import { detectUserPackageManager } from "~/utils/packageManager.js"; -import { getSharedTemplateDir, makeInitRequest, readScaffoldArtifacts } from "./init-fixtures.js"; -import { makeTestLayer } from "./test-layer.js"; - -const proofkitTypegenVersion = getProofkitDependencyVersion(getTypegenVersion()); - -describe("integration scaffold generation", () => { - it("creates a browser scaffold with proofkit.json and env", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-browser-")); - const projectDir = path.join(cwd, "browser-app"); - const consoleTranscript = { - info: [] as string[], - warn: [] as string[], - error: [] as string[], - success: [] as string[], - note: [] as Array<{ message: string; title?: string }>, - }; - const layer = makeTestLayer({ - cwd, - packageManager: detectUserPackageManager(), - console: consoleTranscript, - }); - - const plan = planInit( - makeInitRequest({ - projectName: "browser-app", - scopedAppName: "browser-app", - appDir: "browser-app", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - packageManagerVersion: "11.1.0", - }, - ); - - await Effect.runPromise(layer(executeInitPlan(plan))); - - expect(await fs.pathExists(projectDir)).toBe(true); - expect(await fs.pathExists(path.join(projectDir, "package.json"))).toBe(true); - expect(await fs.pathExists(path.join(projectDir, "proofkit.json"))).toBe(true); - expect(await fs.pathExists(path.join(projectDir, ".env"))).toBe(true); - - const { packageJson, proofkitJson, envFile, claudeFile, cursorIgnoreFile } = - await readScaffoldArtifacts(projectDir); - - expect(packageJson.name).toBe("browser-app"); - expect(packageJson.packageManager).toBeUndefined(); - expect(packageJson.devEngines?.packageManager).toEqual({ - name: "pnpm", - version: "^11.1.0", - onFail: "download", - }); - expect(packageJson.devEngines?.runtime).toEqual({ - name: "node", - version: NODE_RUNTIME_VERSION, - onFail: "download", - }); - expect(packageJson.engines).toEqual({ - node: NODE_RUNTIME_VERSION, - }); - expect(packageJson.proofkitMetadata).toMatchObject({ - scaffoldPackage: "@proofkit/cli", - }); - expect(packageJson.devDependencies["@proofkit/cli"]).toBeUndefined(); - expect(packageJson.devDependencies["@proofkit/typegen"]).toBe(proofkitTypegenVersion); - expect(packageJson.scripts.proofkit).toBeUndefined(); - expect(packageJson.scripts.typegen).toBe("typegen"); - expect(packageJson.scripts["typegen:ui"]).toBe("typegen ui"); - expect(typeof packageJson.proofkitMetadata?.initVersion).toBe("string"); - expect(packageJson.proofkitMetadata?.initVersion).not.toBe(""); - expect(proofkitJson).toMatchObject({ - appType: "browser", - dataSources: [], - envFile: ".env", - }); - expect(claudeFile).toBe("@AGENTS.md\n"); - expect(cursorIgnoreFile).toBe("CLAUDE.md\n"); - expect(envFile).toContain("# When adding additional environment variables"); - expect(consoleTranscript.success.at(-1) ?? "").toContain("Created browser-app"); - }); - - it("creates a webviewer scaffold without leaking state across runs", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-webviewer-")); - const firstDir = path.join(cwd, "first"); - const secondDir = path.join(cwd, "second"); - const layer = makeTestLayer({ - cwd, - packageManager: "pnpm", - }); - - const firstPlan = planInit( - makeInitRequest({ - projectName: "first", - scopedAppName: "first", - appDir: "first", - appType: "webviewer", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("vite-wv"), - }, - ); - - const secondPlan = planInit( - makeInitRequest({ - projectName: "second", - scopedAppName: "second", - appDir: "second", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - await Effect.runPromise(layer(executeInitPlan(firstPlan))); - await Effect.runPromise(layer(executeInitPlan(secondPlan))); - - const firstSettings = await fs.readJson(path.join(firstDir, "proofkit.json")); - const secondSettings = await fs.readJson(path.join(secondDir, "proofkit.json")); - expect(firstSettings.appType).toBe("webviewer"); - expect(secondSettings.appType).toBe("browser"); - }); - - it("creates a webviewer scaffold with ultracite, tanstack wiring, and agent files", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-webviewer-template-")); - const projectDir = path.join(cwd, "webviewer-app"); - const consoleTranscript = { - info: [] as string[], - warn: [] as string[], - error: [] as string[], - success: [] as string[], - note: [] as Array<{ message: string; title?: string }>, - }; - const layer = makeTestLayer({ - cwd, - packageManager: "pnpm", - console: consoleTranscript, - }); - - const plan = planInit( - makeInitRequest({ - projectName: "webviewer-app", - scopedAppName: "webviewer-app", - appDir: "webviewer-app", - appType: "webviewer", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("vite-wv"), - }, - ); - - await Effect.runPromise(layer(executeInitPlan(plan))); - - const { packageJson, agentsFile, claudeFile, cursorIgnoreFile, huskyPreCommitFile, launchConfig } = - await readScaffoldArtifacts(projectDir); - const routerFile = await fs.readFile(path.join(projectDir, "src/router.tsx"), "utf8"); - const mainFile = await fs.readFile(path.join(projectDir, "src/main.tsx"), "utf8"); - const queryDemoFile = await fs.readFile(path.join(projectDir, "src/routes/query-demo.tsx"), "utf8"); - const oxlintConfig = await fs.readFile(path.join(projectDir, "oxlint.config.ts"), "utf8"); - - expect(packageJson.scripts.lint).toBe("ultracite check ."); - expect(packageJson.scripts.format).toBe("ultracite fix ."); - expect(packageJson.scripts.check).toBe("ultracite check"); - expect(packageJson.scripts.fix).toBe("ultracite fix"); - expect(packageJson.scripts.proofkit).toBeUndefined(); - expect(packageJson.dependencies["@tanstack/react-query"]).toBe("^5.90.21"); - expect(packageJson.dependencies["@tanstack/react-router"]).toBe("^1.167.4"); - expect(packageJson.devDependencies.ultracite).toBe("^7.0.0"); - expect(agentsFile).toContain("Use the ProofKit docs as the primary reference"); - expect(claudeFile).toBe("@AGENTS.md\n"); - expect(cursorIgnoreFile).toBe("CLAUDE.md\n"); - expect(launchConfig).toContain('"runtimeExecutable": "pnpm"'); - expect(routerFile).toContain("createHashHistory"); - expect(mainFile).toContain("QueryClientProvider"); - expect(queryDemoFile).toContain("TanStack Query is preconfigured"); - expect(oxlintConfig).toContain('import { defineConfig } from "oxlint"'); - expect(oxlintConfig).toContain('import core from "ultracite/oxlint/core"'); - expect(oxlintConfig).toContain('import react from "ultracite/oxlint/react"'); - expect(oxlintConfig).toContain('"react/react-in-jsx-scope": "off"'); - expect(huskyPreCommitFile).toBe('#!/bin/sh\necho "Running lint-staged..."\npnpm exec lint-staged\n'); - const nextStepsMessage = consoleTranscript.info.at(-1) ?? ""; - expect(nextStepsMessage).not.toContain("Have your agent run this in the new project"); - expect(nextStepsMessage).not.toContain("complete the interactive prompt"); - expect(nextStepsMessage).not.toContain("pnpm proofkit"); - expect(nextStepsMessage).not.toContain("More ProofKit commands"); - expect(nextStepsMessage).toContain("\u001B["); - }); - - it("creates filemaker env and typegen config when explicit hosted inputs are provided", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-new-filemaker-")); - const layer = makeTestLayer({ - cwd, - packageManager: "pnpm", - }); - - const plan = planInit( - makeInitRequest({ - projectName: "filemaker-app", - scopedAppName: "filemaker-app", - appDir: "filemaker-app", - appType: "browser", - ui: "shadcn", - dataSource: "filemaker", - packageManager: "pnpm", - noInstall: true, - noGit: true, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: true, - fileMaker: { - mode: "hosted-otto", - dataSourceName: "filemaker", - envNames: { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }, - server: "https://example.com", - fileName: "Contacts.fmp12", - dataApiKey: "dk_123", - layoutName: "API_Contacts", - schemaName: "Contacts", - }, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - await Effect.runPromise(layer(executeInitPlan(plan))); - - const projectDir = path.join(cwd, "filemaker-app"); - const { packageJson, proofkitJson, envFile, typegenConfig } = await readScaffoldArtifacts(projectDir); - - expect(packageJson.devDependencies["@proofkit/typegen"]).toBe(proofkitTypegenVersion); - expect(proofkitJson.dataSources).toEqual([ - { - type: "fm", - name: "filemaker", - envNames: { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }, - }, - ]); - expect(envFile).toContain("FM_DATABASE=Contacts.fmp12"); - expect(envFile).toContain("FM_SERVER=https://example.com"); - expect(envFile).toContain("OTTO_API_KEY=dk_123"); - expect(typegenConfig).toContain("API_Contacts"); - expect(typegenConfig).toContain("Contacts"); - }); -}); diff --git a/packages/cli/tests/legacy-project-name-utils.test.ts b/packages/cli/tests/legacy-project-name-utils.test.ts deleted file mode 100644 index 07ebbca8..00000000 --- a/packages/cli/tests/legacy-project-name-utils.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { parseNameAndPath } from "~/utils/parseNameAndPath.js"; -import { validateAppName } from "~/utils/validateAppName.js"; - -describe("legacy project name utils", () => { - afterEach(() => { - vi.restoreAllMocks(); - }); - - it("normalizes the current directory name when parsing '.'", () => { - vi.spyOn(process, "cwd").mockReturnValue("/tmp/My App"); - expect(parseNameAndPath(".")).toEqual(["my-app", "."]); - }); - - it("validates the normalized current directory name for '.'", () => { - vi.spyOn(process, "cwd").mockReturnValue("/tmp/My App"); - expect(validateAppName(".")).toBeUndefined(); - }); -}); diff --git a/packages/cli/tests/live-git-init.test.ts b/packages/cli/tests/live-git-init.test.ts deleted file mode 100644 index 5efa6dc2..00000000 --- a/packages/cli/tests/live-git-init.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import os from "node:os"; -import path from "node:path"; -import { Effect } from "effect"; -import fs from "fs-extra"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { executeInitPlan } from "~/core/executeInitPlan.js"; -import { planInit } from "~/core/planInit.js"; -import { makeLiveLayer } from "~/services/live.js"; -import { getSharedTemplateDir, makeInitRequest } from "./init-fixtures.js"; - -const { execaMock, warnMock, successMock } = vi.hoisted(() => ({ - execaMock: vi.fn(), - warnMock: vi.fn(), - successMock: vi.fn(), -})); - -vi.mock("execa", () => ({ - execa: execaMock, -})); - -vi.mock("~/utils/prompts.js", () => ({ - confirmPrompt: vi.fn(), - spinner: vi.fn(), - isCancel: vi.fn(() => false), - log: { - error: vi.fn(), - info: vi.fn(), - success: successMock, - warn: warnMock, - }, - multiSearchSelectPrompt: vi.fn(), - note: vi.fn(), - passwordPrompt: vi.fn(), - searchSelectPrompt: vi.fn(), - selectPrompt: vi.fn(), - textPrompt: vi.fn(), -})); - -describe("live git init", () => { - beforeEach(() => { - vi.clearAllMocks(); - execaMock.mockImplementation((command: string, args: string[]) => { - if (command === "pnpm" && args[0] === "-v") { - return Promise.resolve({ stdout: "11.1.0" }); - } - - if (command === "git" && args[0] === "commit") { - throw new Error("Author identity unknown"); - } - - return Promise.resolve({ stdout: "", stderr: "" }); - }); - }); - - it("warns and continues when initial git commit fails", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "proofkit-live-git-")); - const plan = planInit( - makeInitRequest({ - projectName: "git-warn-app", - scopedAppName: "git-warn-app", - appDir: "git-warn-app", - appType: "browser", - ui: "shadcn", - dataSource: "none", - packageManager: "pnpm", - noInstall: true, - noGit: false, - force: false, - cwd, - importAlias: "~/", - nonInteractive: true, - debug: false, - skipFileMakerSetup: false, - hasExplicitFileMakerInputs: false, - }), - { - templateDir: getSharedTemplateDir("nextjs-shadcn"), - }, - ); - - await expect( - Effect.runPromise(executeInitPlan(plan).pipe(makeLiveLayer({ cwd, debug: false, nonInteractive: true }))), - ).resolves.toBeDefined(); - - expect(execaMock).toHaveBeenCalledWith("git", ["init"], expect.objectContaining({ cwd: plan.targetDir })); - expect(execaMock).toHaveBeenCalledWith("git", ["add", "."], expect.objectContaining({ cwd: plan.targetDir })); - expect(execaMock).toHaveBeenCalledWith( - "git", - ["commit", "-m", "Initial commit"], - expect.objectContaining({ cwd: plan.targetDir }), - ); - expect(warnMock).toHaveBeenCalledWith("Git initial commit failed; continuing without commit."); - expect(successMock).toHaveBeenCalledWith(expect.stringContaining("Created git-warn-app")); - }); -}); diff --git a/packages/cli/tests/non-interactive.test.ts b/packages/cli/tests/non-interactive.test.ts deleted file mode 100644 index ddeafdd9..00000000 --- a/packages/cli/tests/non-interactive.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { detectNonInteractiveTerminal, resolveNonInteractiveMode } from "~/utils/nonInteractive.js"; - -describe("non-interactive detection", () => { - it("treats piped terminals as non-interactive", () => { - expect( - detectNonInteractiveTerminal({ - stdinIsTTY: false, - stdoutIsTTY: true, - env: {}, - }), - ).toBe(true); - }); - - it("treats TERM=dumb as non-interactive", () => { - expect( - detectNonInteractiveTerminal({ - stdinIsTTY: true, - stdoutIsTTY: true, - env: { TERM: "dumb" }, - }), - ).toBe(true); - }); - - it("treats coding-agent env vars as non-interactive even with a tty", () => { - expect( - detectNonInteractiveTerminal({ - stdinIsTTY: true, - stdoutIsTTY: true, - env: { CODEX: "1" }, - }), - ).toBe(true); - }); - - it("treats OPENAI_CODEX as non-interactive even with a tty", () => { - expect( - detectNonInteractiveTerminal({ - stdinIsTTY: true, - stdoutIsTTY: true, - env: { OPENAI_CODEX: "1" }, - }), - ).toBe(true); - }); - - it("keeps real terminals interactive when no signals are present", () => { - expect( - detectNonInteractiveTerminal({ - stdinIsTTY: true, - stdoutIsTTY: true, - env: {}, - }), - ).toBe(false); - }); - - it("lets explicit flags force non-interactive mode", () => { - expect( - resolveNonInteractiveMode({ - nonInteractive: true, - stdinIsTTY: true, - stdoutIsTTY: true, - env: {}, - }), - ).toBe(true); - expect( - resolveNonInteractiveMode({ - CI: true, - stdinIsTTY: true, - stdoutIsTTY: true, - env: {}, - }), - ).toBe(true); - }); -}); diff --git a/packages/cli/tests/ottofms.test.ts b/packages/cli/tests/ottofms.test.ts deleted file mode 100644 index ab60999a..00000000 --- a/packages/cli/tests/ottofms.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import axios from "axios"; -import open from "open"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { getOttoFMSToken } from "~/cli/ottofms.js"; - -vi.mock("axios", () => ({ - default: { - get: vi.fn(), - delete: vi.fn(), - }, - AxiosError: class AxiosError extends Error {}, -})); - -vi.mock("open", () => ({ - default: vi.fn(), -})); - -vi.mock("~/cli/prompts.js", () => ({ - log: { - info: vi.fn(), - }, - spinner: () => ({ - start: vi.fn(), - stop: vi.fn(), - }), -})); - -describe("OttoFMS browser login", () => { - beforeEach(() => { - vi.useFakeTimers(); - vi.mocked(open).mockResolvedValue({} as Awaited>); - vi.mocked(axios.get).mockRejectedValue(new Error("pending")); - vi.mocked(axios.delete).mockResolvedValue({ data: {} }); - }); - - afterEach(() => { - vi.useRealTimers(); - vi.clearAllMocks(); - }); - - it("rejects when login polling times out", async () => { - const tokenPromise = getOttoFMSToken({ - url: new URL("https://example.com"), - }).catch((error: unknown) => error); - - await vi.advanceTimersByTimeAsync(180_000); - - await expect(tokenPromise).resolves.toMatchObject({ - message: "Login timed out", - }); - expect(vi.getTimerCount()).toBe(0); - }); -}); diff --git a/packages/cli/tests/planner.test.ts b/packages/cli/tests/planner.test.ts deleted file mode 100644 index 9b83918b..00000000 --- a/packages/cli/tests/planner.test.ts +++ /dev/null @@ -1,281 +0,0 @@ -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import { NODE_RUNTIME_VERSION } from "~/consts.js"; -import { planInit } from "~/core/planInit.js"; -import { - getFmdapiVersion, - getProofkitDependencyVersion, - getProofkitWebviewerVersion, - getTypegenVersion, - getVersion, -} from "~/utils/getProofKitVersion.js"; -import { makeInitRequest } from "./init-fixtures.js"; - -const proofkitFmdapiVersion = getProofkitDependencyVersion(getFmdapiVersion()); -const proofkitTypegenVersion = getProofkitDependencyVersion(getTypegenVersion()); -const proofkitWebviewerVersion = getProofkitDependencyVersion(getProofkitWebviewerVersion()); -const pnpm11WarningPattern = /pnpm.*11/i; - -describe("planInit", () => { - it("plans a browser scaffold", () => { - const plan = planInit(makeInitRequest(), { - templateDir: "/templates/browser", - packageManagerVersion: "11.0.0", - }); - - expect(plan.targetDir).toBe(path.resolve("/tmp/workspace", "demo-app")); - expect(plan.templateDir).toBe("/templates/browser"); - expect(plan.packageJson.name).toBe("demo-app"); - expect(plan.packageJson.proofkitMetadata).toEqual({ - initVersion: getVersion(), - scaffoldPackage: "@proofkit/cli", - }); - expect(plan.packageJson.devEngines?.packageManager).toEqual({ - name: "pnpm", - version: "^11.0.0", - onFail: "download", - }); - expect(plan.packageJson.devEngines?.runtime).toEqual({ - name: "node", - version: NODE_RUNTIME_VERSION, - onFail: "download", - }); - expect(plan.packageJson.engines).toEqual({ - node: NODE_RUNTIME_VERSION, - }); - expect(plan.settings.appType).toBe("browser"); - expect(plan.packageJson.devDependencies["@proofkit/cli"]).toBeUndefined(); - expect(plan.packageJson.devDependencies["@proofkit/typegen"]).toBe(proofkitTypegenVersion); - expect(plan.tasks.runInstall).toBe(true); - expect(plan.tasks.runUltraciteInit).toBe(true); - expect(plan.tasks.runIntentInstall).toBe(true); - expect(plan.tasks.runFix).toBe(true); - expect(plan.tasks.runLint).toBe(true); - expect(plan.tasks.initializeGit).toBe(true); - expect(plan.tasks.bootstrapFileMaker).toBe(false); - expect(plan.tasks.checkWebViewerAddon).toBe(false); - expect(plan.writes).toContainEqual({ - path: path.resolve("/tmp/workspace", "demo-app", "pnpm-workspace.yaml"), - content: [ - "# This setting defines where in the repo your apps/packages that need installed dependancies exist. This of this as a list of paths to your package.json files. ", - "packages:", - ' - "."', - "", - "allowBuilds:", - ' "@parcel/watcher": true', - ' "esbuild": true', - ' "msgpackr-extract": true', - ' "msw": true', - ' "node": true', - ' "sharp": true', - "", - "trustPolicy: no-downgrade", - "", - "trustPolicyIgnoreAfter: 43200", - "", - "blockExoticSubdeps: true", - "", - ].join("\n"), - }); - }); - - it("plans a webviewer scaffold with no install and no git", () => { - const plan = planInit( - makeInitRequest({ - appType: "webviewer", - dataSource: "none", - noInstall: true, - noGit: true, - }), - { - templateDir: "/templates/webviewer", - packageManagerVersion: "11.0.0", - }, - ); - - expect(plan.packageJson.dependencies["@proofkit/webviewer"]).toBe(proofkitWebviewerVersion); - expect(plan.packageJson.devDependencies["@proofkit/typegen"]).toBe(proofkitTypegenVersion); - expect(plan.tasks.runInstall).toBe(false); - expect(plan.tasks.runUltraciteInit).toBe(true); - expect(plan.tasks.runIntentInstall).toBe(false); - expect(plan.tasks.runFix).toBe(false); - expect(plan.tasks.runLint).toBe(false); - expect(plan.tasks.initializeGit).toBe(false); - expect(plan.tasks.checkWebViewerAddon).toBe(true); - expect(plan.writes).toContainEqual({ - path: path.resolve("/tmp/workspace", "demo-app", "pnpm-workspace.yaml"), - content: [ - "# This setting defines where in the repo your apps/packages that need installed dependancies exist. This of this as a list of paths to your package.json files. ", - "packages:", - ' - "."', - "", - "allowBuilds:", - ' "@parcel/watcher": true', - ' "esbuild": true', - ' "msgpackr-extract": true', - ' "msw": true', - ' "node": true', - ' "sharp": false', - "", - "trustPolicy: no-downgrade", - "", - "trustPolicyIgnoreAfter: 43200", - "", - "blockExoticSubdeps: true", - "", - ].join("\n"), - }); - }); - - it("adds pnpm build approvals for pnpm 10", () => { - const plan = planInit( - makeInitRequest({ - appType: "webviewer", - dataSource: "none", - }), - { - templateDir: "/templates/webviewer", - packageManagerVersion: "10.27.0", - }, - ); - - expect(plan.writes).toContainEqual( - expect.objectContaining({ - path: path.resolve("/tmp/workspace", "demo-app", "pnpm-workspace.yaml"), - content: expect.stringContaining(' "sharp": false'), - }), - ); - }); - - it("warns npm users to use pnpm 11 or greater", () => { - const plan = planInit( - makeInitRequest({ - packageManager: "npm", - }), - { - templateDir: "/templates/browser", - packageManagerVersion: "10.0.0", - }, - ); - - expect(plan.nextSteps).toEqual(expect.arrayContaining([expect.stringMatching(pnpm11WarningPattern)])); - }); - - it("writes npm minimum release age config for npm scaffolds", () => { - const plan = planInit( - makeInitRequest({ - packageManager: "npm", - }), - { - templateDir: "/templates/browser", - packageManagerVersion: "11.10.0", - }, - ); - - expect(plan.writes).toContainEqual({ - path: path.resolve("/tmp/workspace", "demo-app", ".npmrc"), - content: [ - "# Require npm package releases to be at least 24 hours old before install.", - "min-release-age=1", - "", - ].join("\n"), - }); - }); - - it("omits intent install from user-facing next steps", () => { - const cases = [ - ["npm", "npx @tanstack/intent@latest install"], - ["pnpm", "pnpx @tanstack/intent@latest install"], - ["yarn", "yarn dlx @tanstack/intent@latest install"], - ["bun", "bunx @tanstack/intent@latest install"], - ] as const; - - for (const [packageManager, nextStep] of cases) { - const plan = planInit(makeInitRequest({ packageManager }), { - templateDir: "/templates/browser", - }); - - expect(plan.nextSteps).not.toContain(nextStep); - } - }); - - it("adds fmdapi for browser filemaker scaffolds", () => { - const plan = planInit( - makeInitRequest({ - appType: "browser", - dataSource: "filemaker", - }), - { - templateDir: "/templates/browser", - }, - ); - - expect(plan.packageJson.dependencies["@proofkit/fmdapi"]).toBe(proofkitFmdapiVersion); - expect(plan.packageJson.dependencies.zod).toBe("^4"); - expect(plan.packageJson.devDependencies["@proofkit/typegen"]).toBe(proofkitTypegenVersion); - }); - - it("plans filemaker bootstrap and initial codegen when inputs are explicit", () => { - const plan = planInit( - makeInitRequest({ - appType: "webviewer", - dataSource: "filemaker", - hasExplicitFileMakerInputs: true, - fileMaker: { - mode: "hosted-otto", - dataSourceName: "filemaker", - envNames: { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }, - server: "https://example.com", - fileName: "Contacts.fmp12", - dataApiKey: "dk_123", - layoutName: "API_Contacts", - schemaName: "Contacts", - }, - }), - { - templateDir: "/templates/webviewer", - }, - ); - - expect(plan.tasks.bootstrapFileMaker).toBe(true); - expect(plan.tasks.runInitialCodegen).toBe(true); - }); - - it("skips initial codegen for non-interactive webviewer runs without explicit inputs", () => { - const plan = planInit( - makeInitRequest({ - appType: "webviewer", - dataSource: "filemaker", - }), - { - templateDir: "/templates/webviewer", - }, - ); - - expect(plan.tasks.bootstrapFileMaker).toBe(true); - expect(plan.tasks.runInitialCodegen).toBe(false); - }); - - it("skips initial codegen when install is disabled", () => { - const plan = planInit( - makeInitRequest({ - appType: "browser", - dataSource: "filemaker", - noInstall: true, - hasExplicitFileMakerInputs: true, - }), - { - templateDir: "/templates/browser", - }, - ); - - expect(plan.tasks.bootstrapFileMaker).toBe(true); - expect(plan.tasks.runInstall).toBe(false); - expect(plan.tasks.runInitialCodegen).toBe(false); - expect(plan.commands.some((command) => command.type === "codegen")).toBe(false); - }); -}); diff --git a/packages/cli/tests/project-name.test.ts b/packages/cli/tests/project-name.test.ts deleted file mode 100644 index ed10cc3b..00000000 --- a/packages/cli/tests/project-name.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { parseNameAndPath, validateAppName } from "~/utils/projectName.js"; - -describe("projectName utils", () => { - afterEach(() => { - vi.restoreAllMocks(); - }); - - it("normalizes Windows-style separators when parsing the app name and directory", () => { - expect(parseNameAndPath("apps\\my-app")).toEqual(["my-app", "apps/my-app"]); - expect(parseNameAndPath(".\\my-app\\")).toEqual(["my-app", "./my-app"]); - }); - - it("converts spaces to dashes when parsing the app name and directory", () => { - expect(parseNameAndPath("my app")).toEqual(["my-app", "my-app"]); - expect(validateAppName("my app")).toBeUndefined(); - }); - - it("preserves leading directory casing while normalizing only the package segment", () => { - expect(parseNameAndPath("Apps Folder/My App")).toEqual(["my-app", "Apps Folder/my-app"]); - }); - - it("normalizes scoped package segments without lowercasing leading directories", () => { - expect(parseNameAndPath("Apps Folder/@My Scope/My App")).toEqual([ - "@my-scope/my-app", - "Apps Folder/@my-scope/my-app", - ]); - }); - - it("validates the actual current directory name when projectName is '.'", () => { - vi.spyOn(process, "cwd").mockReturnValue("/tmp/My App"); - expect(validateAppName(".")).toBeUndefined(); - }); - - it("accepts '.' when the current directory name is valid", () => { - vi.spyOn(process, "cwd").mockReturnValue("/tmp/my-app"); - expect(validateAppName(".")).toBeUndefined(); - }); - - it("normalizes the current directory name when parsing '.'", () => { - vi.spyOn(process, "cwd").mockReturnValue("/tmp/My App"); - expect(parseNameAndPath(".")).toEqual(["my-app", "."]); - }); -}); diff --git a/packages/cli/tests/prompts.test.ts b/packages/cli/tests/prompts.test.ts deleted file mode 100644 index fd7106cc..00000000 --- a/packages/cli/tests/prompts.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { filterSearchOptions } from "~/utils/prompts.js"; - -describe("filterSearchOptions", () => { - const options = [ - { - value: "Contacts.fmp12", - label: "Contacts.fmp12", - hint: "open", - keywords: ["contacts", "reporting"], - }, - { - value: "Invoices.fmp12", - label: "Invoices.fmp12", - hint: "closed", - keywords: ["billing"], - disabled: "Already connected", - }, - ] as const; - - it("matches on labels, hints, and keywords", () => { - expect(filterSearchOptions(options, "reporting").map((option) => option.value)).toEqual(["Contacts.fmp12"]); - expect(filterSearchOptions(options, "closed").map((option) => option.value)).toEqual(["Invoices.fmp12"]); - }); - - it("returns all options when the search term is empty", () => { - expect(filterSearchOptions(options, "")).toEqual(options); - expect(filterSearchOptions(options, " ")).toEqual(options); - expect(filterSearchOptions(options, undefined)).toEqual(options); - }); - - it("returns an empty list when nothing matches", () => { - expect(filterSearchOptions(options, "missing")).toEqual([]); - }); -}); diff --git a/packages/cli/tests/render-failure.test.ts b/packages/cli/tests/render-failure.test.ts deleted file mode 100644 index 34013d5d..00000000 --- a/packages/cli/tests/render-failure.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Cause } from "effect"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { NonInteractiveInputError } from "~/core/errors.js"; -import { renderFailure } from "~/index.js"; - -describe("renderFailure", () => { - afterEach(() => { - vi.restoreAllMocks(); - }); - - it("renders tagged cli errors without squashing", () => { - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); - - renderFailure( - Cause.fail( - new NonInteractiveInputError({ - message: "typed failure", - }), - ), - false, - ); - - expect(errorSpy).toHaveBeenCalledWith("typed failure"); - }); - - it("renders unknown defects via squash", () => { - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); - - renderFailure(Cause.die(new Error("boom")), false); - - expect(errorSpy).toHaveBeenCalledWith("boom"); - }); -}); diff --git a/packages/cli/tests/resolve-init.test.ts b/packages/cli/tests/resolve-init.test.ts deleted file mode 100644 index 84622dc9..00000000 --- a/packages/cli/tests/resolve-init.test.ts +++ /dev/null @@ -1,859 +0,0 @@ -import { Effect } from "effect"; -import { describe, expect, it } from "vitest"; -import { - CliValidationError, - ExternalCommandError, - FileMakerSetupError, - NonInteractiveInputError, - UserCancelledError, -} from "~/core/errors.js"; -import { resolveInitRequest } from "~/core/resolveInitRequest.js"; -import { getFailure } from "./effect-test-utils.js"; -import { type ConsoleTranscript, makeTestLayer, type PromptTranscript } from "./test-layer.js"; - -describe("resolveInitRequest", () => { - it("uses pnpm when npm invoked and pnpm is installed", async () => { - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: true, - importAlias: "~/", - CI: true, - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "npm", - }), - ), - ); - - expect(request.packageManager).toBe("pnpm"); - }); - - it("aborts interactively when npm invoked and pnpm is missing", async () => { - expect( - await getFailure( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: true, - importAlias: "~/", - CI: false, - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "npm", - nonInteractive: false, - failures: { - packageManagerGetVersion: { - pnpm: new ExternalCommandError({ - message: "pnpm not found", - command: "pnpm", - args: ["-v"], - cwd: "/tmp", - }), - }, - }, - }), - ), - ), - ).toMatchObject( - new UserCancelledError({ - message: "User aborted to install pnpm first.", - }), - ); - }); - - it("continues with npm when warning is ignored", async () => { - const promptTranscript: PromptTranscript = { - text: [], - password: [], - select: [], - searchSelect: [], - multiSearchSelect: [], - confirm: [], - }; - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: true, - importAlias: "~/", - CI: false, - appType: "browser", - dataSource: "none", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "npm", - nonInteractive: false, - prompts: { - select: ["continue"], - }, - promptTranscript, - failures: { - packageManagerGetVersion: { - pnpm: new ExternalCommandError({ - message: "pnpm not found", - command: "pnpm", - args: ["-v"], - cwd: "/tmp", - }), - }, - }, - }), - ), - ); - - expect(request.packageManager).toBe("npm"); - expect(promptTranscript.select[0]?.message).toContain("https://pnpm.io/installation"); - }); - - it("continues with npm non-interactively when pnpm is missing", async () => { - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: true, - importAlias: "~/", - CI: true, - appType: "browser", - dataSource: "none", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "npm", - failures: { - packageManagerGetVersion: { - pnpm: new ExternalCommandError({ - message: "pnpm not found", - command: "pnpm", - args: ["-v"], - cwd: "/tmp", - }), - }, - }, - }), - ), - ); - - expect(request.packageManager).toBe("npm"); - }); - - it("fails for missing project name in non-interactive mode", async () => { - expect( - await getFailure( - resolveInitRequest(undefined, { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - }), - ), - ), - ).toMatchObject( - new NonInteractiveInputError({ - message: "Project name is required in non-interactive mode.", - }), - ); - }); - - it("fails for incomplete non-interactive filemaker inputs", async () => { - expect( - await getFailure( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "browser", - dataSource: "filemaker", - server: "https://example.com", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - }), - ), - ), - ).toMatchObject( - new NonInteractiveInputError({ - message: "Missing required FileMaker inputs in non-interactive mode: --file-name, --data-api-key.", - }), - ); - }); - - it("fails when only one of layout-name and schema-name is provided", async () => { - expect( - await getFailure( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "browser", - dataSource: "filemaker", - layoutName: "API_Contacts", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - }), - ), - ), - ).toMatchObject( - new CliValidationError({ - message: "Both --layout-name and --schema-name must be provided together.", - }), - ); - }); - - it("resolves an interactive filemaker request from prompt responses", async () => { - const request = await Effect.runPromise( - resolveInitRequest(undefined, { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: false, - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - nonInteractive: false, - prompts: { - text: ["interactive-app", "https://fm.example.com", "reportingContacts"], - select: ["webviewer", "hosted"], - searchSelect: ["Contacts.fmp12", "dk_existing", "API_Contacts"], - confirm: [true], - }, - }), - ), - ); - - expect(request.projectName).toBe("interactive-app"); - expect(request.appType).toBe("webviewer"); - expect(request.dataSource).toBe("filemaker"); - expect(request.fileMaker).toMatchObject({ - mode: "hosted-otto", - server: "https://fm.example.com", - fileName: "Contacts.fmp12", - dataApiKey: "dk_existing", - schemaName: "reportingContacts", - }); - }); - - it("marks explicit filemaker inputs in non-interactive mode", async () => { - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "webviewer", - dataSource: "filemaker", - server: "https://fm.example.com", - fileName: "Contacts.fmp12", - dataApiKey: "dk_123", - layoutName: "API_Contacts", - schemaName: "Contacts", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - }), - ), - ); - - expect(request.hasExplicitFileMakerInputs).toBe(true); - expect(request.fileMaker).toMatchObject({ - mode: "hosted-otto", - server: "https://fm.example.com", - fileName: "Contacts.fmp12", - dataApiKey: "dk_123", - layoutName: "API_Contacts", - schemaName: "Contacts", - }); - }); - - it("normalizes a non-interactive layout name to the live FileMaker casing", async () => { - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "webviewer", - dataSource: "filemaker", - server: "https://fm.example.com", - fileName: "Contacts.fmp12", - dataApiKey: "dk_123", - layoutName: "contacts", - schemaName: "Contacts", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - }), - ), - ); - - expect(request.fileMaker).toMatchObject({ - layoutName: "Contacts", - schemaName: "Contacts", - }); - }); - - it("uses local fm http for webviewer setup when available", async () => { - const consoleTranscript: ConsoleTranscript = { - info: [], - warn: [], - error: [], - success: [], - note: [], - }; - const tracker = { - commands: [], - gitInits: 0, - codegens: 0, - filemakerBootstraps: 0, - localFmMcpAuthorizations: [], - }; - - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: false, - appType: "webviewer", - dataSource: "filemaker", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - nonInteractive: false, - console: consoleTranscript, - tracker, - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: ["LocalFile.fmp12"], - }, - }, - }), - ), - ); - - expect(request.fileMaker).toMatchObject({ - mode: "local-fm-mcp", - fileName: "LocalFile.fmp12", - }); - expect(tracker.localFmMcpAuthorizations).toEqual([ - { - clientName: "ProofKit CLI (demo)", - clientDescription: "ProofKit CLI wants to read layouts from your FileMaker file to help set up your project.", - }, - ]); - expect(consoleTranscript.info).toContain("Using ProofKit plugin file: LocalFile.fmp12"); - }); - - it("skips local fm authorization when proofkit token is provided", async () => { - const tracker = { - commands: [], - gitInits: 0, - codegens: 0, - filemakerBootstraps: 0, - localFmMcpAuthorizations: [], - }; - - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: false, - appType: "webviewer", - dataSource: "filemaker", - proofkitToken: "provided-token", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - nonInteractive: false, - tracker, - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: ["LocalFile.fmp12"], - }, - }, - }), - ), - ); - - expect(request.proofkitToken).toBe("provided-token"); - expect(request.fileMaker).toMatchObject({ - mode: "local-fm-mcp", - proofkitToken: "provided-token", - }); - expect(tracker.localFmMcpAuthorizations).toEqual([]); - }); - - it("uses FM_MCP_SESSION_ID as proofkit token fallback", async () => { - const original = process.env.FM_MCP_SESSION_ID; - process.env.FM_MCP_SESSION_ID = "env-token"; - try { - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "webviewer", - dataSource: "filemaker", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: ["LocalFile.fmp12"], - }, - }, - }), - ), - ); - - expect(request.proofkitToken).toBe("env-token"); - } finally { - if (original === undefined) { - delete process.env.FM_MCP_SESSION_ID; - } else { - process.env.FM_MCP_SESSION_ID = original; - } - } - }); - - it("asks which local FileMaker file to use when multiple are open", async () => { - const promptTranscript: PromptTranscript = { - text: [], - password: [], - select: [], - searchSelect: [], - multiSearchSelect: [], - confirm: [], - }; - - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: false, - appType: "webviewer", - dataSource: "filemaker", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - nonInteractive: false, - prompts: { - searchSelect: ["B.fmp12"], - }, - promptTranscript, - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: ["A.fmp12", "B.fmp12"], - }, - }, - }), - ), - ); - - expect(request.fileMaker).toMatchObject({ - mode: "local-fm-mcp", - fileName: "B.fmp12", - }); - expect(promptTranscript.searchSelect).toContain( - "Multiple FileMaker files are open. Which file should ProofKit use?", - ); - }); - - it("fails in non-interactive mode when multiple local FileMaker files are open without --file-name", async () => { - expect( - await getFailure( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "webviewer", - dataSource: "filemaker", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: ["A.fmp12", "B.fmp12"], - }, - }, - }), - ), - ), - ).toMatchObject( - new NonInteractiveInputError({ - message: - "Multiple FileMaker files are connected to the ProofKit plugin. Pass --file-name with one of: A.fmp12, B.fmp12.", - }), - ); - }); - - it("uses --file-name for non-interactive local MCP selection when multiple files are open", async () => { - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "webviewer", - dataSource: "filemaker", - fileName: "B.fmp12", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: ["A.fmp12", "B.fmp12"], - }, - }, - }), - ), - ); - - expect(request.fileMaker).toMatchObject({ - mode: "local-fm-mcp", - fileName: "B.fmp12", - }); - }); - - it("fails when --file-name does not match a connected local FileMaker file", async () => { - expect( - await getFailure( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "webviewer", - dataSource: "filemaker", - fileName: "Missing.fmp12", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: ["A.fmp12", "B.fmp12"], - }, - }, - }), - ), - ), - ).toMatchObject( - new FileMakerSetupError({ - message: - 'FileMaker file "Missing.fmp12" is not currently connected to the ProofKit plugin. Connected files: A.fmp12, B.fmp12.', - }), - ); - }); - - it("propagates a typed hosted FileMaker validation error", async () => { - expect( - await getFailure( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "browser", - dataSource: "filemaker", - server: "https://bad.example.com", - fileName: "Contacts.fmp12", - dataApiKey: "dk_123", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - failures: { - validateHostedServerUrl: new FileMakerSetupError({ - message: "Invalid FileMaker Server URL: https://bad.example.com", - }), - }, - }), - ), - ), - ).toMatchObject( - new FileMakerSetupError({ - message: "Invalid FileMaker Server URL: https://bad.example.com", - }), - ); - }); - - it("prompts to retry when ProofKit plugin is installed but no FileMaker file is connected", async () => { - const promptTranscript: PromptTranscript = { - text: [], - password: [], - select: [], - searchSelect: [], - multiSearchSelect: [], - confirm: [], - }; - const tracker = { - commands: [], - gitInits: 0, - codegens: 0, - filemakerBootstraps: 0, - addonInstalls: 0, - }; - - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: false, - appType: "webviewer", - dataSource: "filemaker", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - nonInteractive: false, - tracker, - prompts: { - select: ["skip"], - }, - promptTranscript, - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: [], - }, - }, - }), - ), - ); - - expect(request.fileMaker).toBeUndefined(); - expect(request.skipFileMakerSetup).toBe(true); - expect(tracker.addonInstalls).toBe(1); - expect(promptTranscript.select).toContainEqual({ - message: - "ProofKit plugin is installed, but no FileMaker file is connected yet. Install the ProofKit Web Viewer add-on in your FileMaker file, run the add-on connection script, then choose how to continue.", - options: ["retry", "hosted", "skip"], - }); - }); - - it("retries local MCP detection, then reports the connected file", async () => { - const consoleTranscript: ConsoleTranscript = { - info: [], - warn: [], - error: [], - success: [], - note: [], - }; - const tracker = { - commands: [], - gitInits: 0, - codegens: 0, - filemakerBootstraps: 0, - addonInstalls: 0, - }; - - const request = await Effect.runPromise( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: false, - appType: "webviewer", - dataSource: "filemaker", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - nonInteractive: false, - tracker, - prompts: { - select: ["retry"], - }, - console: consoleTranscript, - fileMaker: { - localFmMcp: [ - { - healthy: true, - connectedFiles: [], - }, - { - healthy: true, - connectedFiles: ["RetryConnected.fmp12"], - }, - ], - }, - }), - ), - ); - - expect(request.fileMaker).toMatchObject({ - mode: "local-fm-mcp", - fileName: "RetryConnected.fmp12", - }); - expect(tracker.addonInstalls).toBe(2); - expect(consoleTranscript.info).toContain("Using ProofKit plugin file: RetryConnected.fmp12"); - }); - - it("fails with a specific non-interactive error when ProofKit plugin is installed but no FileMaker file is connected", async () => { - expect( - await getFailure( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: true, - appType: "webviewer", - dataSource: "filemaker", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - fileMaker: { - localFmMcp: { - healthy: true, - connectedFiles: [], - }, - }, - }), - ), - ), - ).toMatchObject( - new NonInteractiveInputError({ - message: - "ProofKit plugin was detected, but no FileMaker file is connected. Install the ProofKit plugin, install the ProofKit Web Viewer add-on in your FileMaker file, then run the add-on connection script and rerun. Or pass --server.", - }), - ); - }); - - it("propagates a typed demo deployment error", async () => { - expect( - await getFailure( - resolveInitRequest("demo", { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: false, - appType: "browser", - dataSource: "filemaker", - server: "https://fm.example.com", - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - nonInteractive: false, - prompts: { - searchSelect: ["$deploy-demo"], - }, - failures: { - deployDemoFile: new FileMakerSetupError({ - message: "ProofKit Demo deployment timed out after 5 minutes.", - }), - }, - }), - ), - ), - ).toMatchObject( - new FileMakerSetupError({ - message: "ProofKit Demo deployment timed out after 5 minutes.", - }), - ); - }); - - it("fails with a typed cancelation error when a prompt is cancelled", async () => { - expect( - await getFailure( - resolveInitRequest(undefined, { - noGit: true, - noInstall: true, - force: false, - default: false, - importAlias: "~/", - CI: false, - }).pipe( - makeTestLayer({ - cwd: "/tmp", - packageManager: "pnpm", - nonInteractive: false, - prompts: { - text: ["__cancel__"], - }, - }), - ), - ), - ).toMatchObject( - new UserCancelledError({ - message: "User aborted the operation", - }), - ); - }); -}); diff --git a/packages/cli/tests/setup.ts b/packages/cli/tests/setup.ts deleted file mode 100644 index bff6180a..00000000 --- a/packages/cli/tests/setup.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { execSync } from "node:child_process"; -import path, { join } from "node:path"; -import dotenv from "dotenv"; -import { beforeAll } from "vitest"; - -beforeAll(() => { - // Ensure test environment variables are loaded - dotenv.config({ path: path.resolve(import.meta.dirname, "../.env.test") }); - process.env.PROOFKIT_SKIP_VERSION_CHECK = "1"; -}); - -// Build the CLI before running any tests -execSync("pnpm build", { cwd: join(import.meta.dirname, "..") }); diff --git a/packages/cli/tests/test-layer.ts b/packages/cli/tests/test-layer.ts deleted file mode 100644 index e0c7688a..00000000 --- a/packages/cli/tests/test-layer.ts +++ /dev/null @@ -1,618 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import type { Effect as Fx } from "effect"; -import { Effect, Layer } from "effect"; -import fs from "fs-extra"; -import { - CliContext, - CodegenService, - ConsoleService, - FileMakerService, - FileSystemService, - GitService, - PackageManagerService, - ProcessService, - PromptService, - SettingsService, - TemplateService, -} from "~/core/context.js"; -import { type ExternalCommandError, FileMakerSetupError, FileSystemError, UserCancelledError } from "~/core/errors.js"; -import type { AppType, FileMakerInputs, ProofKitSettings, UIType } from "~/core/types.js"; -import type { PackageManager } from "~/utils/packageManager.js"; -import { createDataSourceEnvNames, updateTypegenConfig } from "~/utils/projectFiles.js"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -export interface PromptScript { - text?: string[]; - select?: Array; - confirm?: Array; - password?: string[]; - searchSelect?: string[]; - multiSearchSelect?: string[][]; -} - -export interface ConsoleTranscript { - info: string[]; - warn: string[]; - error: string[]; - success: string[]; - note: Array<{ message: string; title?: string }>; -} - -export interface PromptTranscript { - text: string[]; - password: string[]; - select: Array<{ - message: string; - options: string[]; - }>; - searchSelect: string[]; - multiSearchSelect: string[]; - confirm: string[]; -} - -export function makeTestLayer(options: { - cwd: string; - packageManager: PackageManager; - nonInteractive?: boolean; - prompts?: PromptScript; - console?: ConsoleTranscript; - failProcessCommand?: string; - promptTranscript?: PromptTranscript; - tracker?: { - commands: string[]; - gitInits: number; - codegens: number; - codegenTokens?: Array; - filemakerBootstraps: number; - addonInstalls?: number; - localFmMcpAuthorizations?: { - clientName: string; - clientDescription: string; - }[]; - }; - fileMaker?: { - localFmMcp?: - | { - healthy: boolean; - baseUrl?: string; - connectedFiles?: string[]; - } - | Array<{ - healthy: boolean; - baseUrl?: string; - connectedFiles?: string[]; - }>; - }; - failures?: { - processRun?: unknown; - gitInitialize?: unknown; - codegenRun?: unknown; - validateHostedServerUrl?: unknown; - deployDemoFile?: unknown; - packageManagerGetVersion?: Partial>; - }; -}) { - const tracker = options.tracker; - const promptScript = { - text: [...(options.prompts?.text ?? [])], - select: [...(options.prompts?.select ?? [])], - confirm: [...(options.prompts?.confirm ?? [])], - password: [...(options.prompts?.password ?? [])], - searchSelect: [...(options.prompts?.searchSelect ?? [])], - multiSearchSelect: [...(options.prompts?.multiSearchSelect ?? [])], - }; - const consoleTranscript = options.console; - let localFmMcpScript: - | Array<{ - healthy: boolean; - baseUrl?: string; - connectedFiles?: string[]; - }> - | undefined; - if (Array.isArray(options.fileMaker?.localFmMcp)) { - localFmMcpScript = [...options.fileMaker.localFmMcp]; - } else if (options.fileMaker?.localFmMcp) { - localFmMcpScript = [options.fileMaker.localFmMcp]; - } else { - localFmMcpScript = []; - } - let lastLocalFmMcp = localFmMcpScript[0]; - - const layer = Layer.mergeAll( - Layer.succeed(CliContext, { - cwd: options.cwd, - debug: false, - nonInteractive: options.nonInteractive ?? true, - packageManager: options.packageManager, - }), - Layer.succeed(PromptService, { - text: ({ message, defaultValue }: { message: string; defaultValue?: string }) => { - options.promptTranscript?.text.push(message); - const next = promptScript.text.shift(); - if (next === "__cancel__") { - return Promise.reject(new UserCancelledError({ message: "User aborted the operation" })); - } - return Promise.resolve(next ?? defaultValue ?? "value"); - }, - password: ({ message }: { message: string }) => { - options.promptTranscript?.password.push(message); - const next = promptScript.password.shift(); - if (next === "__cancel__") { - return Promise.reject(new UserCancelledError({ message: "User aborted the operation" })); - } - return Promise.resolve(next ?? "password"); - }, - select: ({ message, options: selectOptions }: { message: string; options: { value: T }[] }) => { - options.promptTranscript?.select.push({ - message, - options: selectOptions.map((option) => option.value), - }); - const next = promptScript.select.shift(); - if (next === "__cancel__") { - return Promise.reject(new UserCancelledError({ message: "User aborted the operation" })); - } - if (next === "__cancel_value__") { - return Promise.resolve(Symbol.for("@proofkit/new/prompt-cancelled") as unknown as T); - } - if (next) { - const match = selectOptions.find((option) => option.value === next); - if (match) { - return Promise.resolve(match.value); - } - } - return Promise.resolve(selectOptions[0]?.value ?? ("" as T)); - }, - searchSelect: ({ - message, - options: searchOptions, - }: { - message: string; - options: { value: T }[]; - }) => { - options.promptTranscript?.searchSelect.push(message); - const next = promptScript.searchSelect.shift(); - if (next === "__cancel__") { - return Promise.reject(new UserCancelledError({ message: "User aborted the operation" })); - } - if (next) { - const match = searchOptions.find((option) => option.value === next); - if (match) { - return Promise.resolve(match.value); - } - } - return Promise.resolve(searchOptions[0]?.value ?? ("" as T)); - }, - multiSearchSelect: ({ - message, - options: searchOptions, - }: { - message: string; - options: { value: T }[]; - }) => { - options.promptTranscript?.multiSearchSelect.push(message); - const next = promptScript.multiSearchSelect.shift(); - if (next) { - return Promise.resolve( - next.filter((value): value is T => searchOptions.some((option) => option.value === value)), - ); - } - return Promise.resolve(searchOptions.slice(0, 1).map((option) => option.value)); - }, - confirm: ({ message, initialValue }: { message: string; initialValue?: boolean }) => { - options.promptTranscript?.confirm.push(message); - const next = promptScript.confirm.shift(); - if (next === "__cancel_value__") { - return Promise.resolve(Symbol.for("@proofkit/new/prompt-cancelled") as unknown as boolean); - } - return Promise.resolve(next ?? initialValue ?? false); - }, - }), - Layer.succeed(ConsoleService, { - info: (message: string) => { - consoleTranscript?.info.push(message); - }, - warn: (message: string) => { - consoleTranscript?.warn.push(message); - }, - error: (message: string) => { - consoleTranscript?.error.push(message); - }, - success: (message: string) => { - consoleTranscript?.success.push(message); - }, - note: (message: string, title?: string) => { - consoleTranscript?.note.push({ message, title }); - }, - }), - Layer.succeed(FileSystemService, { - exists: (targetPath: string) => - Effect.tryPromise({ - try: () => fs.pathExists(targetPath), - catch: (cause) => - new FileSystemError({ - message: `File system exists failed for ${targetPath}.`, - operation: "exists", - path: targetPath, - cause, - }), - }), - readdir: (targetPath: string) => - Effect.tryPromise({ - try: () => fs.readdir(targetPath), - catch: (cause) => - new FileSystemError({ - message: `File system readdir failed for ${targetPath}.`, - operation: "readdir", - path: targetPath, - cause, - }), - }), - ensureDir: (targetPath: string) => - Effect.tryPromise({ - try: () => fs.ensureDir(targetPath), - catch: (cause) => - new FileSystemError({ - message: `File system ensureDir failed for ${targetPath}.`, - operation: "ensureDir", - path: targetPath, - cause, - }), - }), - emptyDir: (targetPath: string) => - Effect.tryPromise({ - try: () => fs.emptyDir(targetPath), - catch: (cause) => - new FileSystemError({ - message: `File system emptyDir failed for ${targetPath}.`, - operation: "emptyDir", - path: targetPath, - cause, - }), - }), - copyDir: (from: string, to: string, opts?: { overwrite?: boolean }) => - Effect.tryPromise({ - try: () => fs.copy(from, to, { overwrite: opts?.overwrite ?? true }), - catch: (cause) => - new FileSystemError({ - message: `File system copyDir failed for ${from} -> ${to}.`, - operation: "copyDir", - path: `${from} -> ${to}`, - cause, - }), - }), - rename: (from: string, to: string) => - Effect.tryPromise({ - try: () => fs.rename(from, to), - catch: (cause) => - new FileSystemError({ - message: `File system rename failed for ${from} -> ${to}.`, - operation: "rename", - path: `${from} -> ${to}`, - cause, - }), - }), - remove: (targetPath: string) => - Effect.tryPromise({ - try: () => fs.remove(targetPath), - catch: (cause) => - new FileSystemError({ - message: `File system remove failed for ${targetPath}.`, - operation: "remove", - path: targetPath, - cause, - }), - }), - readJson: (targetPath: string) => - Effect.tryPromise({ - try: () => fs.readJson(targetPath) as Promise, - catch: (cause) => - new FileSystemError({ - message: `File system readJson failed for ${targetPath}.`, - operation: "readJson", - path: targetPath, - cause, - }), - }), - writeJson: (targetPath: string, value: unknown) => - Effect.tryPromise({ - try: () => fs.writeJson(targetPath, value, { spaces: 2 }), - catch: (cause) => - new FileSystemError({ - message: `File system writeJson failed for ${targetPath}.`, - operation: "writeJson", - path: targetPath, - cause, - }), - }), - writeFile: (targetPath: string, content: string) => - Effect.tryPromise({ - try: () => fs.writeFile(targetPath, content, "utf8"), - catch: (cause) => - new FileSystemError({ - message: `File system writeFile failed for ${targetPath}.`, - operation: "writeFile", - path: targetPath, - cause, - }), - }), - readFile: (targetPath: string) => - Effect.tryPromise({ - try: () => fs.readFile(targetPath, "utf8"), - catch: (cause) => - new FileSystemError({ - message: `File system readFile failed for ${targetPath}.`, - operation: "readFile", - path: targetPath, - cause, - }), - }), - }), - Layer.succeed(TemplateService, { - getTemplateDir: (appType: AppType, _ui: UIType) => { - let templateName = "nextjs-shadcn"; - if (appType === "webviewer") { - templateName = "vite-wv"; - } - return path.resolve(__dirname, `../template/${templateName}`); - }, - }), - Layer.succeed(PackageManagerService, { - getVersion: (packageManager: PackageManager) => { - const failure = options.failures?.packageManagerGetVersion?.[packageManager]; - if (failure) { - return Effect.fail(failure as ExternalCommandError); - } - return Effect.succeed("11.1.0"); - }, - }), - Layer.succeed(ProcessService, { - run: (command: string, args: string[]) => { - const processCommand = [command, ...args].join(" "); - tracker?.commands.push(processCommand); - const processRunFailure = options.failures?.processRun; - if (options.failProcessCommand === processCommand) { - if (!processRunFailure) { - throw new Error("makeTestLayer requires failures.processRun when failProcessCommand is set."); - } - return Effect.fail(processRunFailure as ExternalCommandError); - } - if (!options.failProcessCommand && processRunFailure) { - return Effect.fail(processRunFailure as ExternalCommandError); - } - return Effect.succeed({ stdout: "", stderr: "" }); - }, - }), - Layer.succeed(GitService, { - initialize: () => { - if (tracker) { - tracker.gitInits += 1; - } - if (options.failures?.gitInitialize) { - return Effect.fail(options.failures.gitInitialize as ExternalCommandError); - } - return Effect.void; - }, - }), - Layer.succeed(SettingsService, { - writeSettings: (projectDir: string, settings: ProofKitSettings) => - Effect.tryPromise({ - try: () => - fs.writeJson(path.join(projectDir, "proofkit.json"), settings, { - spaces: 2, - }), - catch: (cause) => - new FileSystemError({ - message: "Unable to write ProofKit settings.", - operation: "writeSettings", - path: path.join(projectDir, "proofkit.json"), - cause, - }), - }), - appendEnvVars: (projectDir: string, vars: Record) => - Effect.tryPromise({ - try: async () => { - const envPath = path.join(projectDir, ".env"); - const existing = (await fs.pathExists(envPath)) ? await fs.readFile(envPath, "utf8") : ""; - const additions = Object.entries(vars) - .map(([name, value]) => `${name}=${value}`) - .join("\n"); - await fs.writeFile( - envPath, - [existing.trimEnd(), additions].filter(Boolean).join("\n").concat("\n"), - "utf8", - ); - }, - catch: (cause) => - new FileSystemError({ - message: "Unable to append env vars.", - operation: "appendEnvVars", - path: path.join(projectDir, ".env"), - cause, - }), - }), - }), - Layer.succeed(FileMakerService, { - detectLocalFmMcp: () => { - const next = localFmMcpScript.shift() ?? lastLocalFmMcp; - lastLocalFmMcp = next; - return Effect.succeed({ - baseUrl: next?.baseUrl ?? "http://127.0.0.1:1365", - healthy: next?.healthy ?? false, - connectedFiles: next?.connectedFiles ?? [], - }); - }, - installLocalWebViewerAddon: () => { - if (tracker) { - tracker.addonInstalls = (tracker.addonInstalls ?? 0) + 1; - } - return Effect.void; - }, - authorizeLocalFmMcp: (input) => { - tracker?.localFmMcpAuthorizations?.push({ - clientName: input.clientName, - clientDescription: input.clientDescription, - }); - return Effect.succeed({ - sessionToken: "test-session-token", - }); - }, - validateHostedServerUrl: (serverUrl: string) => { - if (options.failures?.validateHostedServerUrl) { - return Effect.fail(options.failures.validateHostedServerUrl as FileMakerSetupError); - } - return Effect.succeed({ - normalizedUrl: serverUrl, - versions: { - fmsVersion: "21.0.0", - ottoVersion: "4.8.0", - }, - }); - }, - getOttoFMSToken: () => Effect.succeed({ token: "admin_token" }), - listFiles: () => Effect.succeed([{ filename: "Contacts.fmp12", status: "open" }]), - listAPIKeys: () => - Effect.succeed([ - { - key: "dk_existing", - user: "Admin", - database: "Contacts.fmp12", - label: "Existing key", - }, - ]), - createDataAPIKeyWithCredentials: () => Effect.succeed({ apiKey: "dk_created" }), - deployDemoFile: () => { - if (options.failures?.deployDemoFile) { - return Effect.fail(options.failures.deployDemoFile as FileMakerSetupError); - } - return Effect.succeed({ - apiKey: "dk_demo", - filename: "ProofKitDemo.fmp12", - }); - }, - listLayouts: () => Effect.succeed(["API_Contacts", "Contacts"]), - createFileMakerBootstrapArtifacts: (settings: ProofKitSettings, inputs: FileMakerInputs, appType: AppType) => { - const envNames = createDataSourceEnvNames("filemaker"); - return Effect.succeed({ - settings: { - ...settings, - dataSources: [ - ...settings.dataSources, - { - type: "fm", - name: "filemaker", - envNames, - }, - ], - }, - envVars: - inputs.mode === "hosted-otto" - ? { - [envNames.database]: inputs.fileName, - [envNames.server]: inputs.server, - [envNames.apiKey]: inputs.dataApiKey, - } - : {}, - envSchemaEntries: - inputs.mode === "hosted-otto" - ? [ - { - name: envNames.database, - zodSchema: 'z.string().endsWith(".fmp12")', - defaultValue: inputs.fileName, - }, - { - name: envNames.server, - zodSchema: "z.string().url()", - defaultValue: inputs.server, - }, - { - name: envNames.apiKey, - zodSchema: 'z.string().startsWith("dk_")', - defaultValue: inputs.dataApiKey, - }, - ] - : [], - typegenConfig: { - mode: inputs.mode, - dataSourceName: "filemaker", - envNames: inputs.mode === "hosted-otto" ? envNames : undefined, - fmMcpBaseUrl: inputs.mode === "local-fm-mcp" ? inputs.fmMcpBaseUrl : undefined, - connectedFileName: inputs.mode === "local-fm-mcp" ? inputs.fileName : undefined, - layoutName: inputs.layoutName, - schemaName: inputs.schemaName, - appType, - }, - }); - }, - bootstrap: (projectDir: string, settings: ProofKitSettings, inputs: FileMakerInputs, appType: AppType) => - Effect.tryPromise({ - try: async () => { - if (tracker) { - tracker.filemakerBootstraps += 1; - } - const nextSettings: ProofKitSettings = { - ...settings, - dataSources: [ - ...settings.dataSources, - { - type: "fm", - name: "filemaker", - envNames: { - database: "FM_DATABASE", - server: "FM_SERVER", - apiKey: "OTTO_API_KEY", - }, - }, - ], - }; - if (inputs.mode === "hosted-otto") { - const envPath = path.join(projectDir, ".env"); - const content = (await fs.readFile(envPath, "utf8")).concat( - `FM_DATABASE=${inputs.fileName}\nFM_SERVER=${inputs.server}\nOTTO_API_KEY=${inputs.dataApiKey}\n`, - ); - await fs.writeFile(envPath, content, "utf8"); - } - await updateTypegenConfig( - { - exists: async (targetPath: string) => fs.pathExists(targetPath), - readFile: async (targetPath: string) => fs.readFile(targetPath, "utf8"), - writeFile: async (targetPath: string, content: string) => fs.writeFile(targetPath, content, "utf8"), - }, - projectDir, - { - appType, - dataSourceName: "filemaker", - envNames: inputs.mode === "hosted-otto" ? createDataSourceEnvNames("filemaker") : undefined, - fmMcpBaseUrl: inputs.mode === "local-fm-mcp" ? inputs.fmMcpBaseUrl : undefined, - connectedFileName: inputs.mode === "local-fm-mcp" ? inputs.fileName : undefined, - layoutName: inputs.layoutName, - schemaName: inputs.schemaName, - }, - ); - return nextSettings; - }, - catch: (cause) => - new FileMakerSetupError({ - message: "Unable to bootstrap FileMaker in test layer.", - cause, - }), - }), - }), - Layer.succeed(CodegenService, { - runInitial: (_projectDir, _packageManager, proofkitToken) => { - if (tracker) { - tracker.codegens += 1; - tracker.codegenTokens?.push(proofkitToken); - } - if (options.failures?.codegenRun) { - return Effect.fail(options.failures.codegenRun as ExternalCommandError); - } - return Effect.void; - }, - }), - ); - - return (effect: Fx.Effect) => Effect.provide(effect, layer); -} diff --git a/packages/cli/tests/test-utils.ts b/packages/cli/tests/test-utils.ts deleted file mode 100644 index ddbd3d79..00000000 --- a/packages/cli/tests/test-utils.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { execSync } from "node:child_process"; -import { readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; - -function execSmokeCommand(command: string, options: Parameters[1]) { - try { - return execSync(command, { - ...options, - stdio: "pipe", - encoding: "utf-8", - }); - } catch (error) { - if (error && typeof error === "object") { - const outputError = error as { stdout?: unknown; stderr?: unknown }; - if (typeof outputError.stdout === "string" && outputError.stdout.length > 0) { - console.error(outputError.stdout); - } - if (typeof outputError.stderr === "string" && outputError.stderr.length > 0) { - console.error(outputError.stderr); - } - } - throw error; - } -} - -/** - * Smoke-test helper only: swap workspace refs to published tags so install/build - * validates what end users can actually fetch from the registry. - */ -function applyPublishedProofkitVersionsForSmoke(projectDir: string): void { - const pkgPath = join(projectDir, "package.json"); - const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); - - const replaceProofkitVersions = (deps: Record | undefined) => { - if (!deps) { - return; - } - for (const name of Object.keys(deps)) { - if (name.startsWith("@proofkit/")) { - console.log(` Replacing ${name}@${deps[name]} with latest`); - deps[name] = "latest"; - } - } - }; - - console.log("Using latest published @proofkit/* versions..."); - replaceProofkitVersions(pkg.dependencies); - replaceProofkitVersions(pkg.devDependencies); - - writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)); -} - -/** - * Verifies that a project at the given directory can be built without errors - * @param projectDir The directory containing the project to build - * @throws If the build fails - */ -export function verifySmokeProjectBuilds(projectDir: string): void { - console.log(`\nVerifying project build in ${projectDir}...`); - - try { - // Smoke tests intentionally validate published package installability. - applyPublishedProofkitVersionsForSmoke(projectDir); - - console.log("Installing dependencies..."); - execSmokeCommand("pnpm install --prefer-offline --no-frozen-lockfile", { - cwd: projectDir, - env: { - ...process.env, - PNPM_DEBUG: "1", // Enable debug logging - }, - }); - - console.log("Building project..."); - execSmokeCommand("pnpm build", { - cwd: projectDir, - env: { - ...process.env, - NEXT_TELEMETRY_DISABLED: "1", - }, - }); - } catch (error) { - console.error("Build process failed:", error); - throw error; - } -} diff --git a/packages/cli/tests/webviewer-apps.test.ts b/packages/cli/tests/webviewer-apps.test.ts deleted file mode 100644 index 531c1ad2..00000000 --- a/packages/cli/tests/webviewer-apps.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { execSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import { beforeEach, describe, expect, it } from "vitest"; -import { TYPEGEN_VERSION } from "../src/package-versions.js"; - -const nonInteractiveDirectoryError = /already exists and isn't empty/; -const expectedTypegenVersion = `^${TYPEGEN_VERSION}`; - -describe("Web Viewer CLI Tests", () => { - const testDir = join(import.meta.dirname, "..", "..", "tmp", "cli-tests"); - const cliPath = join(import.meta.dirname, "..", "bin", "proofkit.cjs"); - const projectName = "test-webviewer-project"; - const projectDir = join(testDir, projectName); - - beforeEach(() => { - if (existsSync(projectDir)) { - rmSync(projectDir, { recursive: true, force: true }); - } - mkdirSync(testDir, { recursive: true }); - }); - - it("should create a webviewer project without FileMaker server setup", () => { - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--app-type webviewer", - "--no-git", - "--no-install", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - }); - }).not.toThrow(); - - expect(existsSync(projectDir)).toBe(true); - expect(existsSync(join(projectDir, "package.json"))).toBe(true); - expect(existsSync(join(projectDir, "proofkit.json"))).toBe(true); - expect(existsSync(join(projectDir, "proofkit-typegen.config.jsonc"))).toBe(true); - - const packageJson = JSON.parse(readFileSync(join(projectDir, "package.json"), "utf-8")); - expect(packageJson.scripts.typegen).toBe("pnpx @proofkit/typegen"); - expect(packageJson.scripts["typegen:ui"]).toBe("pnpx @proofkit/typegen ui"); - expect(packageJson.devDependencies["@proofkit/typegen"]).toBe(expectedTypegenVersion); - - const proofkitConfig = JSON.parse(readFileSync(join(projectDir, "proofkit.json"), "utf-8")); - expect(proofkitConfig.appType).toBe("webviewer"); - expect(proofkitConfig.dataSources).toEqual([]); - }); - - it("should allow agent-only folders in non-interactive mode", () => { - mkdirSync(projectDir, { recursive: true }); - mkdirSync(join(projectDir, ".cursor"), { recursive: true }); - writeFileSync(join(projectDir, ".cursor", "rules.mdc"), "placeholder"); - - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--app-type webviewer", - "--no-git", - "--no-install", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - }); - }).not.toThrow(); - - expect(existsSync(join(projectDir, "package.json"))).toBe(true); - expect(existsSync(join(projectDir, ".cursor"))).toBe(true); - expect(existsSync(join(projectDir, ".cursor", "rules"))).toBe(false); - }); - - it("should allow hidden files in non-interactive mode", () => { - mkdirSync(projectDir, { recursive: true }); - writeFileSync(join(projectDir, ".DS_Store"), "placeholder"); - - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--app-type webviewer", - "--no-git", - "--no-install", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - }); - }).not.toThrow(); - - expect(existsSync(join(projectDir, "package.json"))).toBe(true); - expect(existsSync(join(projectDir, ".DS_Store"))).toBe(true); - }); - - it("should fail in non-interactive mode when .gitignore already exists", () => { - mkdirSync(projectDir, { recursive: true }); - writeFileSync(join(projectDir, ".gitignore"), "node_modules/\n"); - - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--app-type webviewer", - "--no-git", - "--no-install", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - stdio: "pipe", - }); - }).toThrow(nonInteractiveDirectoryError); - - expect(existsSync(join(projectDir, "package.json"))).toBe(false); - }); - - it("should fail without prompting when a non-interactive target directory has real files", () => { - mkdirSync(projectDir, { recursive: true }); - writeFileSync(join(projectDir, "README.md"), "existing content"); - - const command = [ - `node "${cliPath}" init`, - projectName, - "--non-interactive", - "--app-type webviewer", - "--no-git", - "--no-install", - ].join(" "); - - expect(() => { - execSync(command, { - cwd: testDir, - env: process.env, - encoding: "utf-8", - stdio: "pipe", - }); - }).toThrow(nonInteractiveDirectoryError); - - expect(existsSync(join(projectDir, "package.json"))).toBe(false); - }); -}); diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json deleted file mode 100644 index 7f3eae14..00000000 --- a/packages/cli/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "baseUrl": "./", - "exactOptionalPropertyTypes": false, - "paths": { - "~/*": ["./src/*"] - }, - "strictNullChecks": true - }, - "exclude": ["template", "dist"], - "include": ["src", "tests", "tsdown.config.ts", "vitest.config.ts"] -} diff --git a/packages/cli/tsdown.config.ts b/packages/cli/tsdown.config.ts deleted file mode 100644 index 0cb69d19..00000000 --- a/packages/cli/tsdown.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from "tsdown"; - -const isDev = process.env.npm_lifecycle_event === "dev"; - -export default defineConfig({ - clean: true, - entry: ["src/index.ts"], - format: ["esm"], - minify: !isDev, - target: "esnext", - outDir: "dist", - nodeProtocol: false, -}); diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts deleted file mode 100644 index dbff20f2..00000000 --- a/packages/cli/vitest.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import path from "node:path"; -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - resolve: { - alias: { - "~": path.resolve(import.meta.dirname, "src"), - }, - }, - test: { - globals: true, - environment: "node", - include: ["tests/**/*.test.ts"], - exclude: ["**/node_modules/**", "**/dist/**", "tests/**/*.smoke.test.ts"], - fileParallelism: false, - testTimeout: 60_000, - }, -}); diff --git a/packages/cli/vitest.smoke.config.ts b/packages/cli/vitest.smoke.config.ts deleted file mode 100644 index 7a62c5b2..00000000 --- a/packages/cli/vitest.smoke.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { defineConfig } from "vitest/config"; - -const configDir = path.dirname(fileURLToPath(import.meta.url)); - -export default defineConfig({ - resolve: { - alias: { - "~": path.resolve(configDir, "src"), - }, - }, - test: { - globals: true, - environment: "node", - setupFiles: ["./tests/setup.ts"], - include: ["tests/**/*.smoke.test.ts"], - exclude: ["**/node_modules/**", "**/dist/**"], - testTimeout: 180_000, - }, -}); diff --git a/packages/create-proofkit/CHANGELOG.md b/packages/create-proofkit/CHANGELOG.md deleted file mode 100644 index bec232d4..00000000 --- a/packages/create-proofkit/CHANGELOG.md +++ /dev/null @@ -1,49 +0,0 @@ -# create-proofkit - -## 0.1.3 - -### Patch Changes - -- 7d48139: Ship ProofKit CLI in installer and publish CLI packages. - -## 0.1.3-beta.0 - -### Patch Changes - -- 7d48139: Ship ProofKit CLI in installer and publish CLI packages. - -## 0.1.2 - -### Patch Changes - -- b62a73c: Restrict Node engines to 22, 24, or 26. - -## 0.1.1 - -### Patch Changes - -- Maintenance release: internal tooling and docs updates. - -## 0.1.1-beta.1 - -### Patch Changes - -- 7c7f70a: swap docs domain to proofkit.proof.sh - -## 0.1.1-beta.0 - -### Patch Changes - -- 863e1e8: Update tooling to Biome - -## 0.1.0 - -### Minor Changes - -- c348e37: Support @proofkit namespaced packages - -## 0.1.0-beta.0 - -### Minor Changes - -- Support @proofkit namespaced packages diff --git a/packages/create-proofkit/README.md b/packages/create-proofkit/README.md deleted file mode 100644 index 02a7bded..00000000 --- a/packages/create-proofkit/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Create ProofKit - -```bash -npx create proofkit -``` - -This is a simple alias package for the ProofKit CLI. For full documentation, see the [ProofKit Docs](https://proofkit.proof.sh). diff --git a/packages/create-proofkit/package.json b/packages/create-proofkit/package.json deleted file mode 100644 index d090f644..00000000 --- a/packages/create-proofkit/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "create-proofkit", - "version": "0.1.3", - "description": "Create a new ProofKit project", - "type": "module", - "bin": "./src/index.js", - "repository": { - "type": "git", - "url": "https://github.com/proofsh/proofkit.git", - "directory": "packages/create-proofkit" - }, - "files": [ - "src" - ], - "scripts": { - "dev:watch": "node src/index.js", - "pub:release": "npm publish --access public", - "pub:beta": "npm publish --tag beta --access public", - "lint": "cd ../.. && pnpm exec ultracite check packages/create-proofkit/src packages/create-proofkit/tests packages/create-proofkit/package.json packages/create-proofkit/vitest.config.ts", - "lint:summary": "pnpm run lint", - "test": "vitest run --config vitest.config.ts" - }, - "dependencies": { - "execa": "^9.6.1" - }, - "devDependencies": { - "vitest": "^4.0.17" - }, - "engines": { - "node": "^22.0.0 || ^24.0.0" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/create-proofkit/src/getUserPkgManager.js b/packages/create-proofkit/src/getUserPkgManager.js deleted file mode 100644 index ef962926..00000000 --- a/packages/create-proofkit/src/getUserPkgManager.js +++ /dev/null @@ -1,22 +0,0 @@ -/** @typedef {"npm" | "pnpm" | "yarn" | "bun"} PackageManager */ - -/** @returns {PackageManager} */ -export const getUserPkgManager = () => { - // This environment variable is set by npm and yarn but pnpm seems less consistent - const userAgent = process.env.npm_config_user_agent; - - if (userAgent) { - if (userAgent.startsWith("yarn")) { - return "yarn"; - } - if (userAgent.startsWith("pnpm")) { - return "pnpm"; - } - if (userAgent.startsWith("bun")) { - return "bun"; - } - return "npm"; - } - // If no user agent is set, assume pnpm - return "pnpm"; -}; diff --git a/packages/create-proofkit/src/index.js b/packages/create-proofkit/src/index.js deleted file mode 100644 index 3fe032c0..00000000 --- a/packages/create-proofkit/src/index.js +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env node - -import { createRequire } from "node:module"; -import { execa } from "execa"; -import { getUserPkgManager } from "./getUserPkgManager.js"; - -const require = createRequire(import.meta.url); -const packageJson = require("../package.json"); - -function getCliSpecifier() { - const version = packageJson.version; - const tag = version.includes("-") ? "beta" : "latest"; - - return `@proofkit/cli@${tag}`; -} - -async function main() { - const args = process.argv.slice(2); - - const pkgManager = getUserPkgManager(); - let pkgManagerCmd; - if (pkgManager === "pnpm") { - pkgManagerCmd = "pnpx"; - } else if (pkgManager === "bun") { - pkgManagerCmd = "bunx"; - } else if (pkgManager === "npm") { - pkgManagerCmd = "npx"; - } else { - pkgManagerCmd = pkgManager; - } - - try { - await execa(pkgManagerCmd, [getCliSpecifier(), "init", ...args], { - stdio: "inherit", - env: { - ...process.env, - FORCE_COLOR: "1", // Preserve colors in output - }, - }); - } catch { - console.error("Failed to create project"); - process.exit(1); - } -} - -main().catch(() => { - console.error("Failed to create project"); - process.exit(1); -}); diff --git a/packages/create-proofkit/tests/index.test.js b/packages/create-proofkit/tests/index.test.js deleted file mode 100644 index 0b7da027..00000000 --- a/packages/create-proofkit/tests/index.test.js +++ /dev/null @@ -1,104 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import packageJson from "../package.json"; - -const { execaMock } = vi.hoisted(() => ({ - execaMock: vi.fn(), -})); - -vi.mock("execa", () => ({ - execa: execaMock, -})); -const originalArgv = [...process.argv]; -const expectedCliTag = packageJson.version.includes("-") ? "beta" : "latest"; - -let processExitSpy; -let consoleErrorSpy; - -const importWrapperEntry = async () => { - vi.resetModules(); - await import("../src/index.js"); - await Promise.resolve(); -}; - -describe("create-proofkit wrapper", () => { - beforeEach(() => { - execaMock.mockReset(); - execaMock.mockResolvedValue({}); - - processExitSpy = vi.spyOn(process, "exit").mockImplementation(() => undefined); - consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); - }); - - afterEach(() => { - process.argv = [...originalArgv]; - vi.unstubAllEnvs(); - - vi.restoreAllMocks(); - }); - - it.each([ - ["npm", "npm/10.0.0 node/v22.0.0 darwin x64", "npx"], - ["pnpm", "pnpm/9.0.0 node/v22.0.0 darwin x64", "pnpx"], - ["yarn", "yarn/1.22.22 npm/? node/v22.0.0 darwin x64", "yarn"], - ["bun", "bun/1.1.0 node/v22.0.0 darwin x64", "bunx"], - ])("dispatches %s user agents to the expected command", async (_label, userAgent, expectedCommand) => { - vi.stubEnv("npm_config_user_agent", userAgent); - process.argv = ["node", "create-proofkit", "my-app"]; - - await importWrapperEntry(); - - expect(execaMock).toHaveBeenCalledWith( - expectedCommand, - [`@proofkit/cli@${expectedCliTag}`, "init", "my-app"], - expect.objectContaining({ - stdio: "inherit", - env: expect.objectContaining({ - FORCE_COLOR: "1", - }), - }), - ); - expect(processExitSpy).not.toHaveBeenCalled(); - }); - - it("forwards arbitrary init args unchanged", async () => { - const forwardedArgs = ["my-app", "--template", "next", "--install=false", "--yes"]; - vi.stubEnv("npm_config_user_agent", "npm/10.0.0 node/v22.0.0 darwin x64"); - process.argv = ["node", "create-proofkit", ...forwardedArgs]; - - await importWrapperEntry(); - - expect(execaMock).toHaveBeenCalledWith( - "npx", - [`@proofkit/cli@${expectedCliTag}`, "init", ...forwardedArgs], - expect.any(Object), - ); - expect(processExitSpy).not.toHaveBeenCalled(); - }); - - it("falls back to pnpm when no user agent is present", async () => { - vi.stubEnv("npm_config_user_agent", ""); - process.argv = ["node", "create-proofkit", "fallback-app"]; - - await importWrapperEntry(); - - expect(execaMock).toHaveBeenCalledWith( - "pnpx", - [`@proofkit/cli@${expectedCliTag}`, "init", "fallback-app"], - expect.any(Object), - ); - expect(processExitSpy).not.toHaveBeenCalled(); - }); - - it("prints an error and exits when the wrapper command fails", async () => { - execaMock.mockRejectedValueOnce(new Error("boom")); - vi.stubEnv("npm_config_user_agent", "npm/10.0.0 node/v22.0.0 darwin x64"); - process.argv = ["node", "create-proofkit", "broken-app"]; - - await importWrapperEntry(); - - await vi.waitFor(() => { - expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to create project"); - expect(processExitSpy).toHaveBeenCalledWith(1); - }); - }); -}); diff --git a/packages/create-proofkit/vitest.config.ts b/packages/create-proofkit/vitest.config.ts deleted file mode 100644 index f12e4e8a..00000000 --- a/packages/create-proofkit/vitest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - environment: "node", - include: ["tests/**/*.test.js"], - }, -}); diff --git a/packages/typegen/package.json b/packages/typegen/package.json index eb4a0ff3..05658122 100644 --- a/packages/typegen/package.json +++ b/packages/typegen/package.json @@ -42,6 +42,12 @@ "default": "./dist/esm/server/app.js" } }, + "./cli": { + "import": { + "types": "./dist/esm/cli.d.ts", + "default": "./dist/esm/cli.js" + } + }, "./src/types.ts": "./src/types.ts", "./package.json": "./package.json" }, diff --git a/packages/typegen/src/cli.ts b/packages/typegen/src/cli.ts index e8a394ee..a173f022 100644 --- a/packages/typegen/src/cli.ts +++ b/packages/typegen/src/cli.ts @@ -217,7 +217,40 @@ program } }); -program.parse(); +/** + * Run the typegen CLI. Used as the package bin entrypoint and re-exported so + * `@proofkit/cli`'s `typegen` command can delegate without duplicating logic. + * + * @param argv user-supplied args (without the node/script prefix). When omitted, + * the process argv is parsed (bin behavior). + */ +export async function runCli(argv?: readonly string[]) { + if (argv === undefined) { + await program.parseAsync(); + return; + } + await program.parseAsync(argv as string[], { from: "user" }); +} + +function isCliEntrypoint() { + const invokedPath = process.argv[1]; + if (!invokedPath) { + return false; + } + const modulePath = fileURLToPath(import.meta.url); + try { + return fs.realpathSync(invokedPath) === fs.realpathSync(modulePath); + } catch { + return path.resolve(invokedPath) === path.resolve(modulePath); + } +} + +if (isCliEntrypoint()) { + runCli().catch((error: unknown) => { + console.error(error); + process.exit(1); + }); +} function parseEnvs(envPath?: string | undefined) { let actualEnvPath = envPath; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d192dd5b..3b31fcbc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -232,7 +232,7 @@ importers: version: 0.2.1(@types/node@25.0.6)(rollup@4.55.1)(typescript@5.9.3)(vite@6.4.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) better-auth: specifier: ^1.5.4 - version: 1.5.5(@prisma/client@5.22.0(prisma@5.22.0))(mongodb@7.1.0)(mysql2@3.16.0)(next@16.2.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(prisma@5.22.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@4.0.17) + version: 1.5.5(mongodb@7.1.0)(next@16.2.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@4.0.17) c12: specifier: ^3.3.3 version: 3.3.3(magicast@0.3.5) @@ -277,235 +277,6 @@ importers: specifier: ^4.0.17 version: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(@vitest/ui@3.2.4)(happy-dom@20.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) - packages/cli: - devDependencies: - '@auth/drizzle-adapter': - specifier: ^1.11.1 - version: 1.11.1 - '@auth/prisma-adapter': - specifier: ^1.6.0 - version: 1.6.0(@prisma/client@5.22.0(prisma@5.22.0)) - '@better-fetch/fetch': - specifier: 1.1.17 - version: 1.1.17 - '@clack/core': - specifier: ^0.3.5 - version: 0.3.5 - '@clack/prompts': - specifier: ^0.11.0 - version: 0.11.0 - '@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) - '@libsql/client': - specifier: ^0.6.2 - version: 0.6.2 - '@planetscale/database': - specifier: ^1.19.0 - version: 1.19.0 - '@prisma/adapter-planetscale': - specifier: ^5.22.0 - version: 5.22.0(@planetscale/database@1.19.0) - '@prisma/client': - specifier: ^5.22.0 - version: 5.22.0(prisma@5.22.0) - '@proofkit/better-auth': - specifier: workspace:* - version: link:../better-auth - '@proofkit/fmdapi': - specifier: workspace:* - version: link:../fmdapi - '@proofkit/typegen': - specifier: workspace:* - version: link:../typegen - '@proofkit/webviewer': - specifier: workspace:* - version: link:../webviewer - '@rollup/plugin-replace': - specifier: ^6.0.3 - version: 6.0.3(rollup@4.55.1) - '@t3-oss/env-nextjs': - specifier: ^0.10.1 - version: 0.10.1(typescript@5.9.3)(zod@4.3.5) - '@tanstack/react-query': - specifier: ^5.90.16 - version: 5.90.16(react@19.2.3) - '@trpc/client': - specifier: 11.0.0-rc.441 - version: 11.0.0-rc.441(@trpc/server@11.0.0-rc.441) - '@trpc/next': - specifier: 11.0.0-rc.441 - version: 11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/react-query@11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/server@11.0.0-rc.441)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@trpc/server@11.0.0-rc.441)(next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@trpc/react-query': - specifier: 11.0.0-rc.441 - version: 11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/server@11.0.0-rc.441)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@trpc/server': - specifier: 11.0.0-rc.441 - version: 11.0.0-rc.441 - '@types/axios': - specifier: ^0.14.4 - version: 0.14.4 - '@types/fs-extra': - specifier: ^11.0.4 - version: 11.0.4 - '@types/glob': - specifier: ^8.1.0 - version: 8.1.0 - '@types/gradient-string': - specifier: ^1.1.6 - version: 1.1.6 - '@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(@opentelemetry/api@1.9.1)(@types/node@22.19.5)(happy-dom@20.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(msw@2.12.7(@types/node@22.19.5)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2)) - axios: - specifier: ^1.13.2 - version: 1.13.2 - chalk: - specifier: 5.4.1 - version: 5.4.1 - commander: - specifier: ^14.0.2 - version: 14.0.2 - dotenv: - specifier: ^16.6.1 - version: 16.6.1 - drizzle-kit: - specifier: ^0.21.4 - version: 0.21.4 - drizzle-orm: - specifier: ^0.30.10 - version: 0.30.10(@libsql/client@0.6.2)(@opentelemetry/api@1.9.1)(@planetscale/database@1.19.0)(@types/react@19.2.7)(kysely@0.28.12)(mysql2@3.16.0)(postgres@3.4.8)(react@19.2.3) - effect: - specifier: ^3.20.0 - version: 3.20.0 - es-toolkit: - specifier: ^1.43.0 - version: 1.43.0 - execa: - specifier: ^9.6.1 - version: 9.6.1 - fast-glob: - specifier: ^3.3.3 - version: 3.3.3 - fs-extra: - specifier: ^11.3.3 - version: 11.3.3 - glob: - specifier: ^11.1.0 - version: 11.1.0 - gradient-string: - specifier: ^2.0.2 - version: 2.0.2 - handlebars: - specifier: ^4.7.8 - version: 4.7.8 - jiti: - specifier: ^1.21.7 - version: 1.21.7 - jsonc-parser: - specifier: ^3.3.1 - version: 3.3.1 - mysql2: - specifier: ^3.16.0 - version: 3.16.0 - next: - specifier: 16.1.1 - version: 16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0) - next-auth: - specifier: ^4.24.13 - version: 4.24.13(next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - open: - specifier: ^10.2.0 - version: 10.2.0 - ora: - specifier: 6.3.1 - version: 6.3.1 - postgres: - specifier: ^3.4.8 - version: 3.4.8 - prisma: - specifier: ^5.22.0 - version: 5.22.0 - publint: - specifier: ^0.3.16 - version: 0.3.16 - randomstring: - specifier: ^1.3.1 - version: 1.3.1 - react: - specifier: 19.2.3 - version: 19.2.3 - react-dom: - specifier: 19.2.3 - version: 19.2.3(react@19.2.3) - semver: - specifier: ^7.7.3 - version: 7.7.3 - shadcn: - specifier: ^2.10.0 - version: 2.10.0(@types/node@22.19.5)(hono@4.11.3)(typescript@5.9.3) - superjson: - specifier: ^2.2.6 - version: 2.2.6 - tailwindcss: - specifier: ^4.1.18 - version: 4.1.18 - ts-morph: - specifier: ^26.0.0 - version: 26.0.0 - tsdown: - specifier: ^0.14.2 - version: 0.14.2(oxc-resolver@11.16.2)(publint@0.3.16)(typescript@5.9.3) - type-fest: - specifier: ^3.13.1 - version: 3.13.1 - typescript: - specifier: ^5.9.3 - version: 5.9.3 - vitest: - specifier: ^4.0.17 - version: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@22.19.5)(happy-dom@20.1.0)(jiti@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/create-proofkit: - dependencies: - execa: - specifier: ^9.6.1 - version: 9.6.1 - devDependencies: - vitest: - specifier: ^4.0.17 - version: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(happy-dom@20.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) - packages/fmdapi: dependencies: '@standard-schema/spec': @@ -760,7 +531,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.17 - version: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(happy-dom@20.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(@vitest/ui@3.2.4)(happy-dom@20.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) packages/typegen/web: dependencies: @@ -964,42 +735,6 @@ packages: resolution: {integrity: sha512-Izvir8iIoU+X4SKtDAa5kpb+9cpifclzsbA8x/AZY0k0gIfXYQ1fa1B6Epfe6vNA2YfDX8VtrZFgvnXB6aPEoQ==} engines: {node: '>=18'} - '@auth/core@0.29.0': - resolution: {integrity: sha512-MdfEjU6WRjUnPG1+XeBWrTIlAsLZU6V0imCIqVDDDPxLI6UZWldXVqAA2EsDazGofV78jqiCLHaN85mJITDqdg==} - peerDependencies: - '@simplewebauthn/browser': ^9.0.1 - '@simplewebauthn/server': ^9.0.2 - nodemailer: ^6.8.0 - peerDependenciesMeta: - '@simplewebauthn/browser': - optional: true - '@simplewebauthn/server': - optional: true - nodemailer: - optional: true - - '@auth/core@0.41.1': - resolution: {integrity: sha512-t9cJ2zNYAdWMacGRMT6+r4xr1uybIdmYa49calBPeTqwgAFPV/88ac9TEvCR85pvATiSPt8VaNf+Gt24JIT/uw==} - peerDependencies: - '@simplewebauthn/browser': ^9.0.1 - '@simplewebauthn/server': ^9.0.2 - nodemailer: ^7.0.7 - peerDependenciesMeta: - '@simplewebauthn/browser': - optional: true - '@simplewebauthn/server': - optional: true - nodemailer: - optional: true - - '@auth/drizzle-adapter@1.11.1': - resolution: {integrity: sha512-cQTvDZqsyF7RPhDm/B6SvqdVP9EzQhy3oM4Muu7fjjmSYFLbSR203E6dH631ZHSKDn2b4WZkfMnjPDzRsPSAeA==} - - '@auth/prisma-adapter@1.6.0': - resolution: {integrity: sha512-PQU8/Oi5gfjzb0MkhMGVX0Dg877phPzsQdK54+C7ubukCeZPjyvuSAx1vVtWEYVWp2oQvjgG/C6QiDoeC7S10A==} - peerDependencies: - '@prisma/client': '>=2.26.0 || >=3 || >=4 || >=5' - '@aws-crypto/sha256-js@1.2.2': resolution: {integrity: sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g==} @@ -1285,9 +1020,6 @@ packages: '@better-auth/utils@0.3.1': resolution: {integrity: sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg==} - '@better-fetch/fetch@1.1.17': - resolution: {integrity: sha512-MQonMalbmEshb+amuLtCkVjYliyyWrYXZkiMnHLgFjNEBsNBbZSY3+lYsFK1/VxePSupVkUW6xinqhqB3uHE1g==} - '@better-fetch/fetch@1.1.21': resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} @@ -1412,9 +1144,6 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@clack/core@0.3.5': - resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==} - '@clack/core@0.5.0': resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} @@ -1458,97 +1187,6 @@ packages: peerDependencies: react: '>=16.8.0' - '@effect/cli@0.74.0': - resolution: {integrity: sha512-vjMJWJWQ2zMRVcZJj2ZGr7vFgVoX6lsCuqAsNiN2ndWZAidkEJ6g1Euuib2V2nTXeWvRyd3FY2Fw2UvX48Uenw==} - peerDependencies: - '@effect/platform': ^0.95.0 - '@effect/printer': ^0.48.0 - '@effect/printer-ansi': ^0.48.0 - effect: ^3.20.0 - - '@effect/cluster@0.57.0': - resolution: {integrity: sha512-VjZoZ4hmgDb0GtGjktypTk/nArA3ntsXU2O9vOBzDjJLRKVBt7IS0/cllHrHwK5Jxkfz86B2k+Prw4/+nrLFlw==} - peerDependencies: - '@effect/platform': ^0.95.0 - '@effect/rpc': ^0.74.0 - '@effect/sql': ^0.50.0 - '@effect/workflow': ^0.17.0 - effect: ^3.20.0 - - '@effect/experimental@0.59.0': - resolution: {integrity: sha512-XqdBpIH5VLlkRxKlyPYp8TAYUeBPjoWYgtrxDebDab14K4kkrpkHk0ZsmmOiQUZ+LY5veRn/PBSogXor9gtPqg==} - peerDependencies: - '@effect/platform': ^0.95.0 - effect: ^3.20.0 - ioredis: ^5 - lmdb: ^3 - peerDependenciesMeta: - ioredis: - optional: true - lmdb: - optional: true - - '@effect/platform-node-shared@0.58.0': - resolution: {integrity: sha512-kl8ejYM1xvjRlk+4/R1YzB6A3E3hVWY4jIfEl21uu4S43V0S15gHvcur7iMIEXfJTX1a25EKF+Buef+Yv5wZZQ==} - peerDependencies: - '@effect/cluster': ^0.57.0 - '@effect/platform': ^0.95.0 - '@effect/rpc': ^0.74.0 - '@effect/sql': ^0.50.0 - effect: ^3.20.0 - - '@effect/platform-node@0.105.0': - resolution: {integrity: sha512-6JxOLqLJMm+m1ZQavIb75S7YJ4fRvrDaYUZ4rqv2IMq5ZK9HVaU/LeejE9tip9zAG9yNM/6mn183iiIV/xge5w==} - peerDependencies: - '@effect/cluster': ^0.57.0 - '@effect/platform': ^0.95.0 - '@effect/rpc': ^0.74.0 - '@effect/sql': ^0.50.0 - effect: ^3.20.0 - - '@effect/platform@0.95.0': - resolution: {integrity: sha512-WDlRiWRSWlmhCPq09bvAofK0qr5vM4yNklXjoJdZHmugKRRTpN/Okn3ODnjgM/Kb/4hjMrRyrsUeH/Brieq7KA==} - peerDependencies: - effect: ^3.20.0 - - '@effect/printer-ansi@0.48.0': - resolution: {integrity: sha512-CzQ5kiomjR9DZ6LPfKAaWmys6JU65c2Q/VQcTKRK4RfaDWeTAehpAVmgOIyKSPkcr9XBhjo2cJx4xyZ4E5nN7g==} - peerDependencies: - '@effect/typeclass': ^0.39.0 - effect: ^3.20.0 - - '@effect/printer@0.48.0': - resolution: {integrity: sha512-f/+QVyqACuLkoB+HDDX2XxloslmgMDL+C6ecHBV0cB0zJzJmLCOybwOkRcCI2xJ/DWHEIpoRyvq+Bfdza0AIrA==} - peerDependencies: - '@effect/typeclass': ^0.39.0 - effect: ^3.20.0 - - '@effect/rpc@0.74.0': - resolution: {integrity: sha512-EV/cHQqJxLtY+RTlPlVQU1KyTzml1wFne+Sh91RacGRRVh6uTm4UdhRh9TNtbYHD4rM9yD3T6zqUgKr0AH8MvQ==} - peerDependencies: - '@effect/platform': ^0.95.0 - effect: ^3.20.0 - - '@effect/sql@0.50.0': - resolution: {integrity: sha512-sOTzsC+ICASgSmX1RITYo6ut7ZbkX+hMG6YagJEyhtptxco9MgSflpF/ix/L92haJ+YTS5Zur/Dm2bDNfVes4w==} - peerDependencies: - '@effect/experimental': ^0.59.0 - '@effect/platform': ^0.95.0 - effect: ^3.20.0 - - '@effect/typeclass@0.39.0': - resolution: {integrity: sha512-V8qGpm4BTMS4pW9e7aCdxC0sy/TYsdxmnpWtokkNWnggZ6kvh1Psp3AfUuuZLyNmUk4T+lYB/ItEsga/+hryig==} - peerDependencies: - effect: ^3.20.0 - - '@effect/workflow@0.17.0': - resolution: {integrity: sha512-JiayvFTTMrp36P0cVFcgu6Nb7ZJxQv+FRqs3DPORkVAcCZlWOKa3KyuYebN3qZbRsmLzS7cxuC8BAeMuqb+WaQ==} - peerDependencies: - '@effect/experimental': ^0.59.0 - '@effect/platform': ^0.95.0 - '@effect/rpc': ^0.74.0 - effect: ^3.20.0 - '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -1567,20 +1205,6 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild-kit/core-utils@3.3.2': - resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.is' - - '@esbuild-kit/esm-loader@2.6.5': - resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.is' - - '@esbuild/aix-ppc64@0.19.12': - resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -1599,18 +1223,6 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.18.20': - resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.19.12': - resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} @@ -1629,18 +1241,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm@0.18.20': - resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.19.12': - resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} @@ -1659,18 +1259,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-x64@0.18.20': - resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.19.12': - resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} @@ -1689,18 +1277,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.18.20': - resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.19.12': - resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} @@ -1719,18 +1295,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.18.20': - resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.19.12': - resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} @@ -1749,18 +1313,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.18.20': - resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.19.12': - resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} @@ -1779,18 +1331,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.18.20': - resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.19.12': - resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} @@ -1809,18 +1349,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.18.20': - resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.19.12': - resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} @@ -1839,18 +1367,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.18.20': - resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.19.12': - resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} @@ -1869,18 +1385,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.18.20': - resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.19.12': - resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} @@ -1899,18 +1403,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.18.20': - resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.19.12': - resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} @@ -1929,18 +1421,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.18.20': - resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.19.12': - resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} @@ -1959,18 +1439,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.18.20': - resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.19.12': - resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} @@ -1989,18 +1457,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.18.20': - resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.19.12': - resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} @@ -2019,18 +1475,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.18.20': - resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.19.12': - resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} @@ -2049,18 +1493,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.18.20': - resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.19.12': - resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} @@ -2097,18 +1529,6 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.18.20': - resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.19.12': - resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} @@ -2145,18 +1565,6 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.18.20': - resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.19.12': - resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} @@ -2193,18 +1601,6 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.18.20': - resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.19.12': - resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} @@ -2223,18 +1619,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.18.20': - resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.19.12': - resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} @@ -2253,18 +1637,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.18.20': - resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.19.12': - resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} @@ -2283,18 +1655,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.18.20': - resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.19.12': - resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} @@ -2535,33 +1895,11 @@ packages: '@inquirer/ansi@1.0.2': resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} - engines: {node: '>=18'} - - '@inquirer/ansi@2.0.4': - resolution: {integrity: sha512-DpcZrQObd7S0R/U3bFdkcT5ebRwbTTC4D3tCc1vsJizmgPLxNJBo+AAFmrZwe8zk30P2QzgzGWZ3Q9uJwWuhIg==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - - '@inquirer/checkbox@5.1.2': - resolution: {integrity: sha512-PubpMPO2nJgMufkoB3P2wwxNXEMUXnBIKi/ACzDUYfaoPuM7gSTmuxJeMscoLVEsR4qqrCMf5p0SiYGWnVJ8kw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/confirm@5.1.21': - resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true + engines: {node: '>=18'} - '@inquirer/confirm@6.0.10': - resolution: {integrity: sha512-tiNyA73pgpQ0FQ7axqtoLUe4GDYjNCDcVsbgcA5anvwg2z6i+suEngLKKJrWKJolT//GFPZHwN30binDIHgSgQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: @@ -2577,33 +1915,6 @@ packages: '@types/node': optional: true - '@inquirer/core@11.1.7': - resolution: {integrity: sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/editor@5.0.10': - resolution: {integrity: sha512-VJx4XyaKea7t8hEApTw5dxeIyMtWXre2OiyJcICCRZI4hkoHsMoCnl/KbUnJJExLbH9csLLHMVR144ZhFE1CwA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/expand@5.0.10': - resolution: {integrity: sha512-fC0UHJPXsTRvY2fObiwuQYaAnHrp3aDqfwKUJSdfpgv18QUG054ezGbaRNStk/BKD5IPijeMKWej8VV8O5Q/eQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -2613,86 +1924,10 @@ packages: '@types/node': optional: true - '@inquirer/external-editor@2.0.4': - resolution: {integrity: sha512-Prenuv9C1PHj2Itx0BcAOVBTonz02Hc2Nd2DbU67PdGUaqn0nPCnV34oDyyoaZHnmfRxkpuhh/u51ThkrO+RdA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/figures@1.0.15': resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} engines: {node: '>=18'} - '@inquirer/figures@2.0.4': - resolution: {integrity: sha512-eLBsjlS7rPS3WEhmOmh1znQ5IsQrxWzxWDxO51e4urv+iVrSnIHbq4zqJIOiyNdYLa+BVjwOtdetcQx1lWPpiQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - - '@inquirer/input@5.0.10': - resolution: {integrity: sha512-nvZ6qEVeX/zVtZ1dY2hTGDQpVGD3R7MYPLODPgKO8Y+RAqxkrP3i/3NwF3fZpLdaMiNuK0z2NaYIx9tPwiSegQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/number@4.0.10': - resolution: {integrity: sha512-Ht8OQstxiS3APMGjHV0aYAjRAysidWdwurWEo2i8yI5xbhOBWqizT0+MU1S2GCcuhIBg+3SgWVjEoXgfhY+XaA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/password@5.0.10': - resolution: {integrity: sha512-QbNyvIE8q2GTqKLYSsA8ATG+eETo+m31DSR0+AU7x3d2FhaTWzqQek80dj3JGTo743kQc6mhBR0erMjYw5jQ0A==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/prompts@8.3.2': - resolution: {integrity: sha512-yFroiSj2iiBFlm59amdTvAcQFvWS6ph5oKESls/uqPBect7rTU2GbjyZO2DqxMGuIwVA8z0P4K6ViPcd/cp+0w==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/rawlist@5.2.6': - resolution: {integrity: sha512-jfw0MLJ5TilNsa9zlJ6nmRM0ZFVZhhTICt4/6CU2Dv1ndY7l3sqqo1gIYZyMMDw0LvE1u1nzJNisfHEhJIxq5w==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/search@4.1.6': - resolution: {integrity: sha512-3/6kTRae98hhDevENScy7cdFEuURnSpM3JbBNg8yfXLw88HgTOl+neUuy/l9W0No5NzGsLVydhBzTIxZP7yChQ==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - - '@inquirer/select@5.1.2': - resolution: {integrity: sha512-kTK8YIkHV+f02y7bWCh7E0u2/11lul5WepVTclr3UMBtBr05PgcZNWfMa7FY57ihpQFQH/spLMHTcr0rXy50tA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/type@3.0.10': resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} engines: {node: '>=18'} @@ -2702,15 +1937,6 @@ packages: '@types/node': optional: true - '@inquirer/type@4.0.4': - resolution: {integrity: sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -2764,57 +1990,6 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} - '@libsql/client@0.6.2': - resolution: {integrity: sha512-xRNfRLv/dOCbV4qd+M0baQwGmvuZpMd2wG2UAPs8XmcdaPvu5ErkcaeITkxlm3hDEJVabQM1cFhMBxsugWW9fQ==} - - '@libsql/core@0.6.2': - resolution: {integrity: sha512-c2P4M+4u/4b2L02A0KjggO3UW51rGkhxr/7fzJO0fEAqsqrWGxuNj2YtRkina/oxfYvAof6xjp8RucNoIV/Odw==} - - '@libsql/darwin-arm64@0.3.19': - resolution: {integrity: sha512-rmOqsLcDI65zzxlUOoEiPJLhqmbFsZF6p4UJQ2kMqB+Kc0Rt5/A1OAdOZ/Wo8fQfJWjR1IbkbpEINFioyKf+nQ==} - cpu: [arm64] - os: [darwin] - - '@libsql/darwin-x64@0.3.19': - resolution: {integrity: sha512-q9O55B646zU+644SMmOQL3FIfpmEvdWpRpzubwFc2trsa+zoBlSkHuzU9v/C+UNoPHQVRMP7KQctJ455I/h/xw==} - cpu: [x64] - os: [darwin] - - '@libsql/hrana-client@0.6.2': - resolution: {integrity: sha512-MWxgD7mXLNf9FXXiM0bc90wCjZSpErWKr5mGza7ERy2FJNNMXd7JIOv+DepBA1FQTIfI8TFO4/QDYgaQC0goNw==} - - '@libsql/isomorphic-fetch@0.2.5': - resolution: {integrity: sha512-8s/B2TClEHms2yb+JGpsVRTPBfy1ih/Pq6h6gvyaNcYnMVJvgQRY7wAa8U2nD0dppbCuDU5evTNMEhrQ17ZKKg==} - engines: {node: '>=18.0.0'} - - '@libsql/isomorphic-ws@0.1.5': - resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} - - '@libsql/linux-arm64-gnu@0.3.19': - resolution: {integrity: sha512-mgeAUU1oqqh57k7I3cQyU6Trpdsdt607eFyEmH5QO7dv303ti+LjUvh1pp21QWV6WX7wZyjeJV1/VzEImB+jRg==} - cpu: [arm64] - os: [linux] - - '@libsql/linux-arm64-musl@0.3.19': - resolution: {integrity: sha512-VEZtxghyK6zwGzU9PHohvNxthruSxBEnRrX7BSL5jQ62tN4n2JNepJ6SdzXp70pdzTfwroOj/eMwiPt94gkVRg==} - cpu: [arm64] - os: [linux] - - '@libsql/linux-x64-gnu@0.3.19': - resolution: {integrity: sha512-2t/J7LD5w2f63wGihEO+0GxfTyYIyLGEvTFEsMO16XI5o7IS9vcSHrxsvAJs4w2Pf907uDjmc7fUfMg6L82BrQ==} - cpu: [x64] - os: [linux] - - '@libsql/linux-x64-musl@0.3.19': - resolution: {integrity: sha512-BLsXyJaL8gZD8+3W2LU08lDEd9MIgGds0yPy5iNPp8tfhXx3pV/Fge2GErN0FC+nzt4DYQtjL+A9GUMglQefXQ==} - cpu: [x64] - os: [linux] - - '@libsql/win32-x64-msvc@0.3.19': - resolution: {integrity: sha512-ay1X9AobE4BpzG0XPw1gplyLZPGHIgJOovvW23gUrukRegiUP62uzhpRbKNogLlUOynyXeq//prHgPXiebUfWg==} - cpu: [x64] - os: [win32] - '@loaderkit/resolve@1.0.4': resolution: {integrity: sha512-rJzYKVcV4dxJv+vW6jlvagF8zvGxHJ2+HTr1e2qOejfmGhAApgJHl8Aog4mMszxceTRiKTTbnpgmTO1bEZHV/A==} @@ -2866,36 +2041,6 @@ packages: '@mongodb-js/saslprep@1.4.6': resolution: {integrity: sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==} - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} - cpu: [arm64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} - cpu: [x64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} - cpu: [arm64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} - cpu: [arm] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} - cpu: [x64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} - cpu: [x64] - os: [win32] - '@mswjs/interceptors@0.40.0': resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} engines: {node: '>=18'} @@ -2909,9 +2054,6 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@neon-rs/load@0.0.4': - resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} - '@next/swc-darwin-arm64@16.1.1': resolution: {integrity: sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==} engines: {node: '>= 10'} @@ -3121,8 +2263,8 @@ packages: resolution: {integrity: sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA==} engines: {node: '>= 20.0.0'} - '@oxc-project/types@0.133.0': - resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxc-project/types@0.134.0': + resolution: {integrity: sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ==} '@oxc-resolver/binding-android-arm-eabi@11.16.2': resolution: {integrity: sha512-lVJbvydLQIDZHKUb6Zs9Rq80QVTQ9xdCQE30eC9/cjg4wsMoEOg65QZPymUAIVJotpUAWJD0XYcwE7ugfxx5kQ==} @@ -3276,101 +2418,6 @@ packages: cpu: [x64] os: [win32] - '@panva/hkdf@1.2.1': - resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} - - '@parcel/watcher-android-arm64@2.5.6': - resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [android] - - '@parcel/watcher-darwin-arm64@2.5.6': - resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [darwin] - - '@parcel/watcher-darwin-x64@2.5.6': - resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [darwin] - - '@parcel/watcher-freebsd-x64@2.5.6': - resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [freebsd] - - '@parcel/watcher-linux-arm-glibc@2.5.6': - resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-arm-musl@2.5.6': - resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - libc: [musl] - - '@parcel/watcher-linux-arm64-glibc@2.5.6': - resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-arm64-musl@2.5.6': - resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@parcel/watcher-linux-x64-glibc@2.5.6': - resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@parcel/watcher-linux-x64-musl@2.5.6': - resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@parcel/watcher-win32-arm64@2.5.6': - resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [win32] - - '@parcel/watcher-win32-ia32@2.5.6': - resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} - engines: {node: '>= 10.0.0'} - cpu: [ia32] - os: [win32] - - '@parcel/watcher-win32-x64@2.5.6': - resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [win32] - - '@parcel/watcher@2.5.6': - resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} - engines: {node: '>= 10.0.0'} - - '@planetscale/database@1.19.0': - resolution: {integrity: sha512-Tv4jcFUFAFjOWrGSio49H6R2ijALv0ZzVBfJKIdm+kl9X046Fh4LLawrF9OMsglVbK6ukqMJsUCeucGAFTBcMA==} - engines: {node: '>=16'} - '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -3380,38 +2427,6 @@ packages: '@posthog/types@1.372.9': resolution: {integrity: sha512-B7k9S+H9WUKHXxe1HOkQWbpWtMcrBvsodm5stZaLQ3pYxf9TowtwssdzTtX4hHjzSYqgrS1IpNnJX4vs1KgBzA==} - '@prisma/adapter-planetscale@5.22.0': - resolution: {integrity: sha512-4fffELMJCAsvLaO4E4YKw6SsX8z3524f0th8dgagr4/p4PQwOJa8wQUktC3DXZdUGG0jyQUZF9ZYPM5e18UB+A==} - peerDependencies: - '@planetscale/database': ^1.15.0 - - '@prisma/client@5.22.0': - resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} - engines: {node: '>=16.13'} - peerDependencies: - prisma: '*' - peerDependenciesMeta: - prisma: - optional: true - - '@prisma/debug@5.22.0': - resolution: {integrity: sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==} - - '@prisma/driver-adapter-utils@5.22.0': - resolution: {integrity: sha512-Y8msGZl9unmVflXoqdxTejv99UD02Gp4VoIvkyw+YxNUIj7nRz35O7yf5D87qNmTiPMGCS1WjUucG9ZuNq8+tw==} - - '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': - resolution: {integrity: sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==} - - '@prisma/engines@5.22.0': - resolution: {integrity: sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==} - - '@prisma/fetch-engine@5.22.0': - resolution: {integrity: sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==} - - '@prisma/get-platform@5.22.0': - resolution: {integrity: sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==} - '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -4179,97 +3194,97 @@ packages: peerDependencies: react: '>=18.2.0' - '@rolldown/binding-android-arm64@1.0.3': - resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + '@rolldown/binding-android-arm64@1.1.0': + resolution: {integrity: sha512-gCYzGOSkYY6Z034suzd20euvds7lPzMEEla62DJGE/ZAlR4OMBnNbvnBSsIGUCAr52gaWMsloGxP4tVGtN5aCA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.3': - resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + '@rolldown/binding-darwin-arm64@1.1.0': + resolution: {integrity: sha512-JQBD77MNgu+4Z6RAyg69acugdrhhVoWesr3l47zohYZ2YV2fwkWMArkN/2p4l6Ei+Sno7W5q+UsKdVWq5Ens0w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.3': - resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + '@rolldown/binding-darwin-x64@1.1.0': + resolution: {integrity: sha512-p/8cXUTK4Sob604e+xxPhVSbDFf29E6J0l/xESM9rdCfn3aDai3nEs6TnMHUsdD5aNlFz0+gDbiGlozLKGa2YA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.3': - resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + '@rolldown/binding-freebsd-x64@1.1.0': + resolution: {integrity: sha512-KbtOSlVv6fElujiZWMcC3aQYhEwLVVf073RcwlSmpGQvIsKZFUqc0ef4sjUuurRwfbiI6JJXji9DQn+86hawmQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': - resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + '@rolldown/binding-linux-arm-gnueabihf@1.1.0': + resolution: {integrity: sha512-9fZ9i0o0/MQaw7om6Z6TsT7tfCk0jtbEFtC+aPqZL5RNsGWNcHvn6EHgL3dAprjq+AZzPTAQjg2JtpJaMt+6pg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.3': - resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + '@rolldown/binding-linux-arm64-gnu@1.1.0': + resolution: {integrity: sha512-+tog7T66i+yFyIuuAnjL6xmW182W/qTBOUt6BtQ6lBIM1Eikh/fSMz4HGgvuCp5uU0zuIVWng7kDYthjCMOHcg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.3': - resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + '@rolldown/binding-linux-arm64-musl@1.1.0': + resolution: {integrity: sha512-4b7yruLIIj/oZ3GpcLOvxcLCLDMraohn3IhQfN2hBP4w9UekG0DTIajWguJosRGfySf/+h/NwRUiMKoCpxCrqQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.3': - resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + '@rolldown/binding-linux-ppc64-gnu@1.1.0': + resolution: {integrity: sha512-QRDOVZd0bhQ5jLsUsCC3dUxDWdTSVY9WMznowZgCGOrZfLLgctWpelhUASEiBwsXfat/JwYnVd1EaxMhqyT+UQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.3': - resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + '@rolldown/binding-linux-s390x-gnu@1.1.0': + resolution: {integrity: sha512-ypxT+Hq76NFG7woFbNbySnGEajFuYuIXeKz/jfCU+lXUoxfi3zLE6OG/ZQNeK3RpZSYJlAe2bokpsQ046CaieQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.3': - resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + '@rolldown/binding-linux-x64-gnu@1.1.0': + resolution: {integrity: sha512-IdovCmfROFmpTLahdecTDFL74aLERVYN68F/mLZjfVh6LfoplPfI6deyHNMTcVujbokDV5k05XrFO22zfv+qjg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.3': - resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + '@rolldown/binding-linux-x64-musl@1.1.0': + resolution: {integrity: sha512-pcA8xlFp2tyk9T2R6Fi/rPe3bQ1MA+sSMDNUU5Ogu80GHOatkE4P8YCreGAvZErm5Ho2YRXnyvNrWiRncfVysQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.3': - resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + '@rolldown/binding-openharmony-arm64@1.1.0': + resolution: {integrity: sha512-4+fexHayrLCWpriPh4c6dNvL4an34DEZCG7zOM/FD5QNF6h8DT+bDXzyB/kfC8lDJbaFb7jKShtnjDQFXVQEjg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.3': - resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + '@rolldown/binding-wasm32-wasi@1.1.0': + resolution: {integrity: sha512-SbL++MNmOw6QamrwIGDMSSfM4ceTzFr+RjbOExJSLLBinScU4WI5OdA413h1qwPw2yH7lVF1+H4svQ+6mSXKTQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.3': - resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + '@rolldown/binding-win32-arm64-msvc@1.1.0': + resolution: {integrity: sha512-+xTE6XC7wBgk0VKRXGG+QAnyW5S9b8vfsFpiMjf0waQTmSQSU8onsH/beyZ8X4aXVveJnotiy7VDjLOaW8bTrg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.3': - resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + '@rolldown/binding-win32-x64-msvc@1.1.0': + resolution: {integrity: sha512-Ogji1TQNqH3ACLnYr+1Ns1nyrJ0CO2P585u9Hsh02pXvtFiFpgtgT2b3P4PnCOU86VVCvqtAeCN4OftMT8KU4w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -4280,15 +3295,6 @@ packages: '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} - '@rollup/plugin-replace@6.0.3': - resolution: {integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -4550,24 +3556,6 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@t3-oss/env-core@0.10.1': - resolution: {integrity: sha512-GcKZiCfWks5CTxhezn9k5zWX3sMDIYf6Kaxy2Gx9YEQftFcz8hDRN56hcbylyAO3t4jQnQ5ifLawINsNgCDpOg==} - peerDependencies: - typescript: '>=5.0.0' - zod: ^3.0.0 - peerDependenciesMeta: - typescript: - optional: true - - '@t3-oss/env-nextjs@0.10.1': - resolution: {integrity: sha512-iy2qqJLnFh1RjEWno2ZeyTu0ufomkXruUsOZludzDIroUabVvHsrSjtkHqwHp1/pgPUzN3yBRHMILW162X7x2Q==} - peerDependencies: - typescript: '>=5.0.0' - zod: ^3.0.0 - peerDependenciesMeta: - typescript: - optional: true - '@tabler/icons-react@3.36.1': resolution: {integrity: sha512-/8nOXeNeMoze9xY/QyEKG65wuvRhkT3q9aytaur6Gj8bYU2A98YVJyLc9MRmc5nVvpy+bRlrrwK/Ykr8WGyUWg==} peerDependencies: @@ -4769,39 +3757,6 @@ packages: '@textlint/utils@15.5.2': resolution: {integrity: sha512-g7Zs8QDZJspno9C7i5iGdwuJ07SxCHWIgxpAebwX503sup4w5IOo3q0X/LxRdl1F4sSAFw/m2glYZomOieNPCw==} - '@trpc/client@11.0.0-rc.441': - resolution: {integrity: sha512-O9zHP7JcK35jO5G8BoW304WdRcHW1TKZae2QDU65KvfMxosbmqY2ajwAgs6CxTS45c1PuF9vI0kXtP52e3FYgQ==} - peerDependencies: - '@trpc/server': 11.0.0-rc.441+0c4a58144 - - '@trpc/next@11.0.0-rc.441': - resolution: {integrity: sha512-C8x7mK2jD+am+vYcFQz5uIRuJFL3gcZ6AyWdZKvI0J6lzd607LXN20V2dgIutclHBj3zOjfWMLRnKilH67JRFw==} - peerDependencies: - '@tanstack/react-query': ^5.49.2 - '@trpc/client': 11.0.0-rc.441+0c4a58144 - '@trpc/react-query': 11.0.0-rc.441+0c4a58144 - '@trpc/server': 11.0.0-rc.441+0c4a58144 - next: '*' - react: '>=16.8.0' - react-dom: '>=16.8.0' - peerDependenciesMeta: - '@tanstack/react-query': - optional: true - '@trpc/react-query': - optional: true - - '@trpc/react-query@11.0.0-rc.441': - resolution: {integrity: sha512-VZm17FyQ/imz5S2pdJe6Qt9Od3JH1jDL8SlI5LZJ/ZXm+vdIbY3KJO1GOBKWfUw5oewaAE/QZJS0xVZqpIvw7g==} - peerDependencies: - '@tanstack/react-query': ^5.49.2 - '@trpc/client': 11.0.0-rc.441+0c4a58144 - '@trpc/server': 11.0.0-rc.441+0c4a58144 - react: '>=18.2.0' - react-dom: '>=18.2.0' - - '@trpc/server@11.0.0-rc.441': - resolution: {integrity: sha512-H0NN85JDgDlvG9tHW9efygLJZbVkszLagm5VeLD8MuhXqqKU+WyMTqb4D8rI560dse4dMC3lI5IoXaCEXMoznA==} - '@trpc/server@11.8.1': resolution: {integrity: sha512-P4rzZRpEL7zDFgjxK65IdyH0e41FMFfTkQkuq0BA5tKcr7E6v9/v38DEklCpoDN6sPiB1Sigy/PUEzHENhswDA==} peerDependencies: @@ -4852,10 +3807,6 @@ packages: '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} - '@types/axios@0.14.4': - resolution: {integrity: sha512-9JgOaunvQdsQ/qW2OPmE5+hCeUB52lQSolecrFrthct55QekhmXEwT203s20RL+UHtCQc15y3VXpby9E7Kkh/g==} - deprecated: This is a stub types definition. axios provides its own type definitions, so you do not need this installed. - '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -4871,9 +3822,6 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/cookie@0.6.0': - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -4892,12 +3840,6 @@ packages: '@types/fs-extra@11.0.4': resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} - '@types/glob@8.1.0': - resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} - - '@types/gradient-string@1.1.6': - resolution: {integrity: sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ==} - '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -4925,9 +3867,6 @@ packages: '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - '@types/minimatch@5.1.2': - resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} - '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -4949,9 +3888,6 @@ packages: '@types/prompts@2.4.9': resolution: {integrity: sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==} - '@types/randomstring@1.3.0': - resolution: {integrity: sha512-kCP61wludjY7oNUeFiMxfswHB3Wn/aC03Cu82oQsNTO6OCuhVN/rCbBs68Cq6Nkgjmp2Sh3Js6HearJPkk7KQA==} - '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -4969,9 +3905,6 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} - '@types/tinycolor2@1.4.6': - resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} - '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -5255,16 +4188,6 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - - aws-ssl-profiles@1.1.2: - resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} - engines: {node: '>= 6.0.0'} - - axios@1.13.2: - resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} - bail@1.0.5: resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==} @@ -5399,9 +4322,6 @@ packages: resolution: {integrity: sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==} engines: {node: '>=20.19.0'} - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer@4.9.2: resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} @@ -5529,10 +4449,6 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - cli-color@2.0.4: - resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==} - engines: {node: '>=0.10'} - cli-cursor@4.0.0: resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5614,10 +4530,6 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -5629,10 +4541,6 @@ packages: resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} - commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} - compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} @@ -5675,10 +4583,6 @@ packages: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} - cookie@0.6.0: - resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} - engines: {node: '>= 0.6'} - cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} @@ -5687,10 +4591,6 @@ packages: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} - copy-anything@4.0.5: - resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} - engines: {node: '>=18'} - core-js@3.49.0: resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} @@ -5722,10 +4622,6 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - d@1.0.2: - resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} - engines: {node: '>=0.12'} - data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -5778,14 +4674,6 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - - denque@2.1.0: - resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} - engines: {node: '>=0.10'} - depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -5801,10 +4689,6 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} - detect-libc@2.0.2: - resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} - engines: {node: '>=8'} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -5831,9 +4715,6 @@ packages: resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} engines: {node: '>=0.3.1'} - difflib@0.2.4: - resolution: {integrity: sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -5849,94 +4730,6 @@ packages: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} - dreamopt@0.8.0: - resolution: {integrity: sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg==} - engines: {node: '>=0.4.0'} - - drizzle-kit@0.21.4: - resolution: {integrity: sha512-Nxcc1ONJLRgbhmR+azxjNF9Ly9privNLEIgW53c92whb4xp8jZLH1kMCh/54ci1mTMuYxPdOukqLwJ8wRudNwA==} - hasBin: true - - drizzle-orm@0.30.10: - resolution: {integrity: sha512-IRy/QmMWw9lAQHpwbUh1b8fcn27S/a9zMIzqea1WNOxK9/4EB8gIo+FZWLiPXzl2n9ixGSv8BhsLZiOppWEwBw==} - peerDependencies: - '@aws-sdk/client-rds-data': '>=3' - '@cloudflare/workers-types': '>=3' - '@electric-sql/pglite': '>=0.1.1' - '@libsql/client': '*' - '@neondatabase/serverless': '>=0.1' - '@op-engineering/op-sqlite': '>=2' - '@opentelemetry/api': ^1.4.1 - '@planetscale/database': '>=1' - '@types/better-sqlite3': '*' - '@types/pg': '*' - '@types/react': '>=18' - '@types/sql.js': '*' - '@vercel/postgres': '>=0.8.0' - '@xata.io/client': '*' - better-sqlite3: '>=7' - bun-types: '*' - expo-sqlite: '>=13.2.0' - knex: '*' - kysely: '*' - mysql2: '>=2' - pg: '>=8' - postgres: '>=3' - react: '>=18' - sql.js: '>=1' - sqlite3: '>=5' - peerDependenciesMeta: - '@aws-sdk/client-rds-data': - optional: true - '@cloudflare/workers-types': - optional: true - '@electric-sql/pglite': - optional: true - '@libsql/client': - optional: true - '@neondatabase/serverless': - optional: true - '@op-engineering/op-sqlite': - optional: true - '@opentelemetry/api': - optional: true - '@planetscale/database': - optional: true - '@types/better-sqlite3': - optional: true - '@types/pg': - optional: true - '@types/react': - optional: true - '@types/sql.js': - optional: true - '@vercel/postgres': - optional: true - '@xata.io/client': - optional: true - better-sqlite3: - optional: true - bun-types: - optional: true - expo-sqlite: - optional: true - knex: - optional: true - kysely: - optional: true - mysql2: - optional: true - pg: - optional: true - postgres: - optional: true - react: - optional: true - sql.js: - optional: true - sqlite3: - optional: true - dts-resolver@2.1.3: resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} engines: {node: '>=20.19.0'} @@ -6004,10 +4797,6 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} - env-paths@3.0.0: - resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -6030,48 +4819,15 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - es-toolkit@1.43.0: resolution: {integrity: sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==} - es5-ext@0.10.64: - resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} - engines: {node: '>=0.10'} - - es6-iterator@2.0.3: - resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} - - es6-symbol@3.1.4: - resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} - engines: {node: '>=0.12'} - - es6-weak-map@2.0.3: - resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==} - esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} esast-util-from-js@2.0.1: resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} - esbuild-register@3.6.0: - resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} - peerDependencies: - esbuild: '>=0.12 <1' - - esbuild@0.18.20: - resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} - engines: {node: '>=12'} - hasBin: true - - esbuild@0.19.12: - resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -6106,10 +4862,6 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - esniff@2.0.1: - resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} - engines: {node: '>=0.10'} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -6146,9 +4898,6 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - event-emitter@0.3.5: - resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} - eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -6189,9 +4938,6 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} - ext@1.7.0: - resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} - extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -6218,18 +4964,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-string-truncated-width@3.0.3: - resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} - - fast-string-width@3.0.2: - resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fast-wrap-ansi@0.2.0: - resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} - fast-xml-parser@5.3.3: resolution: {integrity: sha512-2O3dkPAAC6JavuMm8+4+pgTk+5hoAs+CjZ+sWcQLkX9+/tHRuTkQh/Oaifr8qDmZ8iEHb771Ea6G8CdwkrgvYA==} hasBin: true @@ -6277,9 +5014,6 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} - find-my-way-ts@0.1.6: - resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} - find-up-simple@1.0.1: resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} engines: {node: '>=18'} @@ -6301,23 +5035,10 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - follow-redirects@1.15.11: - resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} - format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} @@ -6471,9 +5192,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - generate-function@2.3.1: - resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -6544,22 +5262,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - gradient-string@2.0.2: - resolution: {integrity: sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw==} - engines: {node: '>=10'} - graphql@16.12.0: resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} - engines: {node: '>=0.4.7'} - hasBin: true - - hanji@0.0.5: - resolution: {integrity: sha512-Abxw1Lq+TnYiL4BueXqMau222fPSPMFtya8HdpWsz/xVAhifXou71mPh/kY2+08RgFcVccjG3uZHs6K5HAe3zw==} - happy-dom@20.1.0: resolution: {integrity: sha512-ebvqjBqzenBk2LjzNEAzoj7yhw7rW/R2/wVevMu6Mrq3MXtcI/RUz4+ozpcOcqVLEWPqLfg2v9EAU7fFXZUUJw==} engines: {node: '>=20.0.0'} @@ -6576,10 +5282,6 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - hashery@1.5.0: resolution: {integrity: sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==} engines: {node: '>=20'} @@ -6610,9 +5312,6 @@ packages: headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} - heap@0.2.7: - resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} - highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} @@ -6698,10 +5397,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@4.1.3: - resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -6804,15 +5499,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} - is-promise@2.2.2: - resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} - is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-property@1.0.2: - resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} - is-regexp@3.1.0: resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} engines: {node: '>=12'} @@ -6837,10 +5526,6 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} - is-what@5.5.0: - resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} - engines: {node: '>=18'} - is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -6909,18 +5594,9 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} - jose@4.15.9: - resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} - - jose@5.10.0: - resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} - jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} - js-base64@3.7.8: - resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} - js-cookie@2.2.1: resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} @@ -6940,10 +5616,6 @@ packages: engines: {node: '>=6'} hasBin: true - json-diff@0.9.0: - resolution: {integrity: sha512-cVnggDrVkAAA3OvFfHpFEhOnmcsUpleEKq4d4O8sQWWSH40MBrWstKigVB1kGrgLWzuom+7rRdaCsnBD6VyObQ==} - hasBin: true - json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -6989,9 +5661,6 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} - kubernetes-types@1.30.0: - resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} - kysely@0.28.12: resolution: {integrity: sha512-kWiueDWXhbCchgiotwXkwdxZE/6h56IHAeFWg4euUfW0YsmO9sxbAxzx1KLLv2lox15EfuuxHQvgJ1qIfZuHGw==} engines: {node: '>=20.0.0'} @@ -7000,11 +5669,6 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libsql@0.3.19: - resolution: {integrity: sha512-Aj5cQ5uk/6fHdmeW0TiXK42FqUlwx7ytmMLPSaUQPin5HKKKuUPD62MAbN4OEweGBBI7q1BekoEN4gPUEL6MZA==} - cpu: [x64, arm64, wasm32] - os: [darwin, linux, win32] - lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -7159,13 +5823,6 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - lru-queue@0.1.0: - resolution: {integrity: sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==} - - lru.min@1.1.3: - resolution: {integrity: sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==} - engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} - lucide-react@0.511.0: resolution: {integrity: sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==} peerDependencies: @@ -7299,10 +5956,6 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} - memoizee@0.4.17: - resolution: {integrity: sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==} - engines: {node: '>=0.12'} - memory-pager@1.5.0: resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} @@ -7453,27 +6106,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - mime-types@3.0.2: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} - mime@3.0.0: - resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} - engines: {node: '>=10.0.0'} - hasBin: true - mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -7565,13 +6205,6 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - msgpackr-extract@3.0.3: - resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} - hasBin: true - - msgpackr@1.11.9: - resolution: {integrity: sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw==} - msw@2.12.7: resolution: {integrity: sha512-retd5i3xCZDVWMYjHEVuKTmhqY8lSsxujjVrZiGbbdoxxIBg5S7rCuYy/YQpfrTYIxpd/o0Kyb/3H+1udBMoYg==} engines: {node: '>=18'} @@ -7585,28 +6218,13 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - multipasta@0.2.7: - resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==} - mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} - mute-stream@3.0.0: - resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} - engines: {node: ^20.17.0 || >=22.9.0} - - mysql2@3.16.0: - resolution: {integrity: sha512-AEGW7QLLSuSnjCS4pk3EIqOmogegmze9h8EyrndavUQnIUcfkVal/sK7QznE+a3bc6rzPbAiui9Jcb+96tPwYA==} - engines: {node: '>= 8.0'} - mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - named-placeholders@1.1.6: - resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} - engines: {node: '>=8.0.0'} - nano-spawn@2.0.0: resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==} engines: {node: '>=20.17'} @@ -7624,36 +6242,16 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - neotraverse@0.6.18: resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} engines: {node: '>= 10'} - next-auth@4.24.13: - resolution: {integrity: sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==} - peerDependencies: - '@auth/core': 0.34.3 - next: ^12.2.5 || ^13 || ^14 || ^15 || ^16 - nodemailer: ^7.0.7 - react: ^17.0.2 || ^18 || ^19 - react-dom: ^17.0.2 || ^18 || ^19 - peerDependenciesMeta: - '@auth/core': - optional: true - nodemailer: - optional: true - next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next-tick@1.1.0: - resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} - next@16.1.1: resolution: {integrity: sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==} engines: {node: '>=20.9.0'} @@ -7696,9 +6294,6 @@ packages: sass: optional: true - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -7724,10 +6319,6 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-gyp-build-optional-packages@5.2.2: - resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} - hasBin: true - node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -7761,23 +6352,10 @@ packages: engines: {node: ^14.16.0 || >=16.10.0} hasBin: true - oauth4webapi@2.17.0: - resolution: {integrity: sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==} - - oauth4webapi@3.8.3: - resolution: {integrity: sha512-pQ5BsX3QRTgnt5HxgHwgunIRaDXBdkT23tf8dfzmtTIL2LTpdmxgbpbBm0VgFWAIDlezQvQCTgnVIUmHupXHxw==} - - oauth@0.9.15: - resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-hash@2.2.0: - resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} - engines: {node: '>= 6'} - object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -7794,10 +6372,6 @@ packages: ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} - oidc-token-hash@5.2.0: - resolution: {integrity: sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==} - engines: {node: ^10.13.0 || >=12.0.0} - on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -7827,9 +6401,6 @@ packages: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} - openid-client@5.7.1: - resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -8015,28 +6586,9 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - postgres@3.4.8: - resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==} - engines: {node: '>=12'} - posthog-js@1.372.9: resolution: {integrity: sha512-qFhTxxrONCO4YBubuEp6/f3beRPku8OqgfHwpSro/XcDU0oPXMVyvsXGUnxhjaq4PvQb79PwvquAX0/HIGcnWg==} - preact-render-to-string@5.2.3: - resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==} - peerDependencies: - preact: ^10.26.10 - - preact-render-to-string@5.2.6: - resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} - peerDependencies: - preact: ^10.26.10 - - preact-render-to-string@6.5.11: - resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} - peerDependencies: - preact: ^10.26.10 - preact@10.28.2: resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==} @@ -8058,18 +6610,10 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - pretty-format@3.8.0: - resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} - pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} - prisma@5.22.0: - resolution: {integrity: sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==} - engines: {node: '>=16.13'} - hasBin: true - prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -8085,9 +6629,6 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} @@ -8136,13 +6677,6 @@ packages: '@types/react-dom': optional: true - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - - randomstring@1.3.1: - resolution: {integrity: sha512-lgXZa80MUkjWdE7g2+PZ1xDLzc7/RokXVEQOv5NN2UOTChW1I8A9gha5a9xYBOqgaSoI6uJikDmCU8PyRdArRQ==} - hasBin: true - range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -8371,8 +6905,8 @@ packages: vue-tsc: optional: true - rolldown@1.0.3: - resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + rolldown@1.1.0: + resolution: {integrity: sha512-zpMvlJhs5PkXRTtKc0CaLBVI9AR/VDiJFpM+kx//hgToEca7FgMlGjaRIisXBcb19T76LswgmKECSQ96hjWr5A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -8437,9 +6971,6 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - seq-queue@0.0.5: - resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} - serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} @@ -8543,9 +7074,6 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -8581,10 +7109,6 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sqlstring@2.3.3: - resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} - engines: {node: '>= 0.6'} - stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -8697,10 +7221,6 @@ packages: babel-plugin-macros: optional: true - superjson@2.2.6: - resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} - engines: {node: '>=16'} - supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -8765,19 +7285,12 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - timers-ext@0.1.8: - resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} - engines: {node: '>=0.12'} - tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinycolor2@1.6.0: - resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -8786,9 +7299,6 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinygradient@1.1.5: - resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} - tinyrainbow@1.2.0: resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} engines: {node: '>=14.0.0'} @@ -8816,9 +7326,6 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - toml@3.0.0: - resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} - totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -8970,9 +7477,6 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} - type@2.7.3: - resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} - typescript@5.4.2: resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} engines: {node: '>=14.17'} @@ -8996,11 +7500,6 @@ packages: ufo@1.6.2: resolution: {integrity: sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==} - uglify-js@3.19.3: - resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} - engines: {node: '>=0.8.0'} - hasBin: true - ultracite@7.0.8: resolution: {integrity: sha512-b98lKaVl3UtH1TF6gZjhPQgtx063i0XpdV1nHEfexHsLyLaaosqU9FT8Tw/HwQkb/UmJ8WihKndur0bSUT0BYw==} hasBin: true @@ -9023,10 +7522,6 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@7.24.4: - resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} - engines: {node: '>=20.18.1'} - unfetch@4.2.0: resolution: {integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==} @@ -9137,11 +7632,6 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). - hasBin: true - validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -9333,9 +7823,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - wrap-ansi@5.1.0: resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==} engines: {node: '>=6'} @@ -9477,41 +7964,6 @@ snapshots: typescript: 5.6.1-rc validate-npm-package-name: 5.0.1 - '@auth/core@0.29.0': - dependencies: - '@panva/hkdf': 1.2.1 - '@types/cookie': 0.6.0 - cookie: 0.6.0 - jose: 5.10.0 - oauth4webapi: 2.17.0 - preact: 10.28.2 - preact-render-to-string: 5.2.3(preact@10.28.2) - - '@auth/core@0.41.1': - dependencies: - '@panva/hkdf': 1.2.1 - jose: 6.1.3 - oauth4webapi: 3.8.3 - preact: 10.28.2 - preact-render-to-string: 6.5.11(preact@10.28.2) - - '@auth/drizzle-adapter@1.11.1': - dependencies: - '@auth/core': 0.41.1 - transitivePeerDependencies: - - '@simplewebauthn/browser' - - '@simplewebauthn/server' - - nodemailer - - '@auth/prisma-adapter@1.6.0(@prisma/client@5.22.0(prisma@5.22.0))': - dependencies: - '@auth/core': 0.29.0 - '@prisma/client': 5.22.0(prisma@5.22.0) - transitivePeerDependencies: - - '@simplewebauthn/browser' - - '@simplewebauthn/server' - - nodemailer - '@aws-crypto/sha256-js@1.2.2': dependencies: '@aws-crypto/util': 1.2.2 @@ -9838,13 +8290,10 @@ snapshots: '@better-auth/utils': 0.3.1 mongodb: 7.1.0 - '@better-auth/prisma-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@5.22.0(prisma@5.22.0))(prisma@5.22.0)': + '@better-auth/prisma-adapter@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1)': dependencies: '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 - optionalDependencies: - '@prisma/client': 5.22.0(prisma@5.22.0) - prisma: 5.22.0 '@better-auth/telemetry@1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))': dependencies: @@ -9854,8 +8303,6 @@ snapshots: '@better-auth/utils@0.3.1': {} - '@better-fetch/fetch@1.1.17': {} - '@better-fetch/fetch@1.1.21': {} '@biomejs/biome@2.3.11': @@ -10051,11 +8498,6 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 - '@clack/core@0.3.5': - dependencies: - picocolors: 1.1.1 - sisteransi: 1.0.5 - '@clack/core@0.5.0': dependencies: picocolors: 1.1.1 @@ -10106,102 +8548,6 @@ snapshots: react: 19.2.3 tslib: 2.8.1 - '@effect/cli@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(@effect/printer-ansi@0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0))(@effect/printer@0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/platform': 0.95.0(effect@3.20.0) - '@effect/printer': 0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0) - '@effect/printer-ansi': 0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0) - effect: 3.20.0 - ini: 4.1.3 - toml: 3.0.0 - yaml: 2.8.2 - - '@effect/cluster@0.57.0(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/platform': 0.95.0(effect@3.20.0) - '@effect/rpc': 0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - '@effect/sql': 0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - '@effect/workflow': 0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) - effect: 3.20.0 - kubernetes-types: 1.30.0 - - '@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/platform': 0.95.0(effect@3.20.0) - effect: 3.20.0 - uuid: 11.1.0 - - '@effect/platform-node-shared@0.58.0(@effect/cluster@0.57.0(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/cluster': 0.57.0(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) - '@effect/platform': 0.95.0(effect@3.20.0) - '@effect/rpc': 0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - '@effect/sql': 0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - '@parcel/watcher': 2.5.6 - effect: 3.20.0 - multipasta: 0.2.7 - ws: 8.19.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@effect/platform-node@0.105.0(@effect/cluster@0.57.0(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/cluster': 0.57.0(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) - '@effect/platform': 0.95.0(effect@3.20.0) - '@effect/platform-node-shared': 0.58.0(@effect/cluster@0.57.0(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/workflow@0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0) - '@effect/rpc': 0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - '@effect/sql': 0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - effect: 3.20.0 - mime: 3.0.0 - undici: 7.24.4 - ws: 8.19.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@effect/platform@0.95.0(effect@3.20.0)': - dependencies: - effect: 3.20.0 - find-my-way-ts: 0.1.6 - msgpackr: 1.11.9 - multipasta: 0.2.7 - - '@effect/printer-ansi@0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/printer': 0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0) - '@effect/typeclass': 0.39.0(effect@3.20.0) - effect: 3.20.0 - - '@effect/printer@0.48.0(@effect/typeclass@0.39.0(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/typeclass': 0.39.0(effect@3.20.0) - effect: 3.20.0 - - '@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/platform': 0.95.0(effect@3.20.0) - effect: 3.20.0 - msgpackr: 1.11.9 - - '@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/experimental': 0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - '@effect/platform': 0.95.0(effect@3.20.0) - effect: 3.20.0 - uuid: 11.1.0 - - '@effect/typeclass@0.39.0(effect@3.20.0)': - dependencies: - effect: 3.20.0 - - '@effect/workflow@0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(@effect/platform@0.95.0(effect@3.20.0))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': - dependencies: - '@effect/experimental': 0.59.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - '@effect/platform': 0.95.0(effect@3.20.0) - '@effect/rpc': 0.74.0(@effect/platform@0.95.0(effect@3.20.0))(effect@3.20.0) - effect: 3.20.0 - '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -10234,19 +8580,6 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild-kit/core-utils@3.3.2': - dependencies: - esbuild: 0.18.20 - source-map-support: 0.5.21 - - '@esbuild-kit/esm-loader@2.6.5': - dependencies: - '@esbuild-kit/core-utils': 3.3.2 - get-tsconfig: 4.13.0 - - '@esbuild/aix-ppc64@0.19.12': - optional: true - '@esbuild/aix-ppc64@0.25.12': optional: true @@ -10256,12 +8589,6 @@ snapshots: '@esbuild/aix-ppc64@0.27.7': optional: true - '@esbuild/android-arm64@0.18.20': - optional: true - - '@esbuild/android-arm64@0.19.12': - optional: true - '@esbuild/android-arm64@0.25.12': optional: true @@ -10271,12 +8598,6 @@ snapshots: '@esbuild/android-arm64@0.27.7': optional: true - '@esbuild/android-arm@0.18.20': - optional: true - - '@esbuild/android-arm@0.19.12': - optional: true - '@esbuild/android-arm@0.25.12': optional: true @@ -10286,12 +8607,6 @@ snapshots: '@esbuild/android-arm@0.27.7': optional: true - '@esbuild/android-x64@0.18.20': - optional: true - - '@esbuild/android-x64@0.19.12': - optional: true - '@esbuild/android-x64@0.25.12': optional: true @@ -10301,12 +8616,6 @@ snapshots: '@esbuild/android-x64@0.27.7': optional: true - '@esbuild/darwin-arm64@0.18.20': - optional: true - - '@esbuild/darwin-arm64@0.19.12': - optional: true - '@esbuild/darwin-arm64@0.25.12': optional: true @@ -10316,12 +8625,6 @@ snapshots: '@esbuild/darwin-arm64@0.27.7': optional: true - '@esbuild/darwin-x64@0.18.20': - optional: true - - '@esbuild/darwin-x64@0.19.12': - optional: true - '@esbuild/darwin-x64@0.25.12': optional: true @@ -10331,12 +8634,6 @@ snapshots: '@esbuild/darwin-x64@0.27.7': optional: true - '@esbuild/freebsd-arm64@0.18.20': - optional: true - - '@esbuild/freebsd-arm64@0.19.12': - optional: true - '@esbuild/freebsd-arm64@0.25.12': optional: true @@ -10346,12 +8643,6 @@ snapshots: '@esbuild/freebsd-arm64@0.27.7': optional: true - '@esbuild/freebsd-x64@0.18.20': - optional: true - - '@esbuild/freebsd-x64@0.19.12': - optional: true - '@esbuild/freebsd-x64@0.25.12': optional: true @@ -10361,12 +8652,6 @@ snapshots: '@esbuild/freebsd-x64@0.27.7': optional: true - '@esbuild/linux-arm64@0.18.20': - optional: true - - '@esbuild/linux-arm64@0.19.12': - optional: true - '@esbuild/linux-arm64@0.25.12': optional: true @@ -10376,12 +8661,6 @@ snapshots: '@esbuild/linux-arm64@0.27.7': optional: true - '@esbuild/linux-arm@0.18.20': - optional: true - - '@esbuild/linux-arm@0.19.12': - optional: true - '@esbuild/linux-arm@0.25.12': optional: true @@ -10391,12 +8670,6 @@ snapshots: '@esbuild/linux-arm@0.27.7': optional: true - '@esbuild/linux-ia32@0.18.20': - optional: true - - '@esbuild/linux-ia32@0.19.12': - optional: true - '@esbuild/linux-ia32@0.25.12': optional: true @@ -10406,12 +8679,6 @@ snapshots: '@esbuild/linux-ia32@0.27.7': optional: true - '@esbuild/linux-loong64@0.18.20': - optional: true - - '@esbuild/linux-loong64@0.19.12': - optional: true - '@esbuild/linux-loong64@0.25.12': optional: true @@ -10421,12 +8688,6 @@ snapshots: '@esbuild/linux-loong64@0.27.7': optional: true - '@esbuild/linux-mips64el@0.18.20': - optional: true - - '@esbuild/linux-mips64el@0.19.12': - optional: true - '@esbuild/linux-mips64el@0.25.12': optional: true @@ -10436,12 +8697,6 @@ snapshots: '@esbuild/linux-mips64el@0.27.7': optional: true - '@esbuild/linux-ppc64@0.18.20': - optional: true - - '@esbuild/linux-ppc64@0.19.12': - optional: true - '@esbuild/linux-ppc64@0.25.12': optional: true @@ -10451,12 +8706,6 @@ snapshots: '@esbuild/linux-ppc64@0.27.7': optional: true - '@esbuild/linux-riscv64@0.18.20': - optional: true - - '@esbuild/linux-riscv64@0.19.12': - optional: true - '@esbuild/linux-riscv64@0.25.12': optional: true @@ -10466,12 +8715,6 @@ snapshots: '@esbuild/linux-riscv64@0.27.7': optional: true - '@esbuild/linux-s390x@0.18.20': - optional: true - - '@esbuild/linux-s390x@0.19.12': - optional: true - '@esbuild/linux-s390x@0.25.12': optional: true @@ -10481,12 +8724,6 @@ snapshots: '@esbuild/linux-s390x@0.27.7': optional: true - '@esbuild/linux-x64@0.18.20': - optional: true - - '@esbuild/linux-x64@0.19.12': - optional: true - '@esbuild/linux-x64@0.25.12': optional: true @@ -10505,12 +8742,6 @@ snapshots: '@esbuild/netbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.18.20': - optional: true - - '@esbuild/netbsd-x64@0.19.12': - optional: true - '@esbuild/netbsd-x64@0.25.12': optional: true @@ -10529,12 +8760,6 @@ snapshots: '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.18.20': - optional: true - - '@esbuild/openbsd-x64@0.19.12': - optional: true - '@esbuild/openbsd-x64@0.25.12': optional: true @@ -10553,12 +8778,6 @@ snapshots: '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/sunos-x64@0.18.20': - optional: true - - '@esbuild/sunos-x64@0.19.12': - optional: true - '@esbuild/sunos-x64@0.25.12': optional: true @@ -10568,12 +8787,6 @@ snapshots: '@esbuild/sunos-x64@0.27.7': optional: true - '@esbuild/win32-arm64@0.18.20': - optional: true - - '@esbuild/win32-arm64@0.19.12': - optional: true - '@esbuild/win32-arm64@0.25.12': optional: true @@ -10583,12 +8796,6 @@ snapshots: '@esbuild/win32-arm64@0.27.7': optional: true - '@esbuild/win32-ia32@0.18.20': - optional: true - - '@esbuild/win32-ia32@0.19.12': - optional: true - '@esbuild/win32-ia32@0.25.12': optional: true @@ -10598,12 +8805,6 @@ snapshots: '@esbuild/win32-ia32@0.27.7': optional: true - '@esbuild/win32-x64@0.18.20': - optional: true - - '@esbuild/win32-x64@0.19.12': - optional: true - '@esbuild/win32-x64@0.25.12': optional: true @@ -10772,7 +8973,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.8.1 + '@emnapi/runtime': 1.10.0 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -10786,17 +8987,6 @@ snapshots: '@inquirer/ansi@1.0.2': {} - '@inquirer/ansi@2.0.4': {} - - '@inquirer/checkbox@5.1.2(@types/node@22.19.5)': - dependencies: - '@inquirer/ansi': 2.0.4 - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/figures': 2.0.4 - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - '@inquirer/confirm@5.1.21(@types/node@22.19.5)': dependencies: '@inquirer/core': 10.3.2(@types/node@22.19.5) @@ -10812,13 +9002,6 @@ snapshots: '@types/node': 25.0.6 optional: true - '@inquirer/confirm@6.0.10(@types/node@22.19.5)': - dependencies: - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - '@inquirer/core@10.3.2(@types/node@22.19.5)': dependencies: '@inquirer/ansi': 1.0.2 @@ -10846,33 +9029,6 @@ snapshots: '@types/node': 25.0.6 optional: true - '@inquirer/core@11.1.7(@types/node@22.19.5)': - dependencies: - '@inquirer/ansi': 2.0.4 - '@inquirer/figures': 2.0.4 - '@inquirer/type': 4.0.4(@types/node@22.19.5) - cli-width: 4.1.0 - fast-wrap-ansi: 0.2.0 - mute-stream: 3.0.0 - signal-exit: 4.1.0 - optionalDependencies: - '@types/node': 22.19.5 - - '@inquirer/editor@5.0.10(@types/node@22.19.5)': - dependencies: - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/external-editor': 2.0.4(@types/node@22.19.5) - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - - '@inquirer/expand@5.0.10(@types/node@22.19.5)': - dependencies: - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - '@inquirer/external-editor@1.0.3(@types/node@22.19.5)': dependencies: chardet: 2.1.1 @@ -10880,78 +9036,8 @@ snapshots: optionalDependencies: '@types/node': 22.19.5 - '@inquirer/external-editor@2.0.4(@types/node@22.19.5)': - dependencies: - chardet: 2.1.1 - iconv-lite: 0.7.2 - optionalDependencies: - '@types/node': 22.19.5 - '@inquirer/figures@1.0.15': {} - '@inquirer/figures@2.0.4': {} - - '@inquirer/input@5.0.10(@types/node@22.19.5)': - dependencies: - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - - '@inquirer/number@4.0.10(@types/node@22.19.5)': - dependencies: - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - - '@inquirer/password@5.0.10(@types/node@22.19.5)': - dependencies: - '@inquirer/ansi': 2.0.4 - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - - '@inquirer/prompts@8.3.2(@types/node@22.19.5)': - dependencies: - '@inquirer/checkbox': 5.1.2(@types/node@22.19.5) - '@inquirer/confirm': 6.0.10(@types/node@22.19.5) - '@inquirer/editor': 5.0.10(@types/node@22.19.5) - '@inquirer/expand': 5.0.10(@types/node@22.19.5) - '@inquirer/input': 5.0.10(@types/node@22.19.5) - '@inquirer/number': 4.0.10(@types/node@22.19.5) - '@inquirer/password': 5.0.10(@types/node@22.19.5) - '@inquirer/rawlist': 5.2.6(@types/node@22.19.5) - '@inquirer/search': 4.1.6(@types/node@22.19.5) - '@inquirer/select': 5.1.2(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - - '@inquirer/rawlist@5.2.6(@types/node@22.19.5)': - dependencies: - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - - '@inquirer/search@4.1.6(@types/node@22.19.5)': - dependencies: - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/figures': 2.0.4 - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - - '@inquirer/select@5.1.2(@types/node@22.19.5)': - dependencies: - '@inquirer/ansi': 2.0.4 - '@inquirer/core': 11.1.7(@types/node@22.19.5) - '@inquirer/figures': 2.0.4 - '@inquirer/type': 4.0.4(@types/node@22.19.5) - optionalDependencies: - '@types/node': 22.19.5 - '@inquirer/type@3.0.10(@types/node@22.19.5)': optionalDependencies: '@types/node': 22.19.5 @@ -10961,10 +9047,6 @@ snapshots: '@types/node': 25.0.6 optional: true - '@inquirer/type@4.0.4(@types/node@22.19.5)': - optionalDependencies: - '@types/node': 22.19.5 - '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -11026,61 +9108,6 @@ snapshots: '@keyv/serialize@1.1.1': {} - '@libsql/client@0.6.2': - dependencies: - '@libsql/core': 0.6.2 - '@libsql/hrana-client': 0.6.2 - js-base64: 3.7.8 - libsql: 0.3.19 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@libsql/core@0.6.2': - dependencies: - js-base64: 3.7.8 - - '@libsql/darwin-arm64@0.3.19': - optional: true - - '@libsql/darwin-x64@0.3.19': - optional: true - - '@libsql/hrana-client@0.6.2': - dependencies: - '@libsql/isomorphic-fetch': 0.2.5 - '@libsql/isomorphic-ws': 0.1.5 - js-base64: 3.7.8 - node-fetch: 3.3.2 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@libsql/isomorphic-fetch@0.2.5': {} - - '@libsql/isomorphic-ws@0.1.5': - dependencies: - '@types/ws': 8.18.1 - ws: 8.19.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@libsql/linux-arm64-gnu@0.3.19': - optional: true - - '@libsql/linux-arm64-musl@0.3.19': - optional: true - - '@libsql/linux-x64-gnu@0.3.19': - optional: true - - '@libsql/linux-x64-musl@0.3.19': - optional: true - - '@libsql/win32-x64-msvc@0.3.19': - optional: true - '@loaderkit/resolve@1.0.4': dependencies: '@braidai/lang': 1.1.2 @@ -11254,24 +9281,6 @@ snapshots: dependencies: sparse-bitfield: 3.0.3 - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - optional: true - '@mswjs/interceptors@0.40.0': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -11295,8 +9304,6 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@neon-rs/load@0.0.4': {} - '@next/swc-darwin-arm64@16.1.1': optional: true @@ -11448,7 +9455,7 @@ snapshots: '@orama/orama@3.1.18': {} - '@oxc-project/types@0.133.0': {} + '@oxc-project/types@0.134.0': {} '@oxc-resolver/binding-android-arm-eabi@11.16.2': optional: true @@ -11536,70 +9543,6 @@ snapshots: '@oxlint/win32-x64@1.39.0': optional: true - '@panva/hkdf@1.2.1': {} - - '@parcel/watcher-android-arm64@2.5.6': - optional: true - - '@parcel/watcher-darwin-arm64@2.5.6': - optional: true - - '@parcel/watcher-darwin-x64@2.5.6': - optional: true - - '@parcel/watcher-freebsd-x64@2.5.6': - optional: true - - '@parcel/watcher-linux-arm-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-arm-musl@2.5.6': - optional: true - - '@parcel/watcher-linux-arm64-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-arm64-musl@2.5.6': - optional: true - - '@parcel/watcher-linux-x64-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-x64-musl@2.5.6': - optional: true - - '@parcel/watcher-win32-arm64@2.5.6': - optional: true - - '@parcel/watcher-win32-ia32@2.5.6': - optional: true - - '@parcel/watcher-win32-x64@2.5.6': - optional: true - - '@parcel/watcher@2.5.6': - dependencies: - detect-libc: 2.1.2 - is-glob: 4.0.3 - node-addon-api: 7.1.1 - picomatch: 4.0.3 - optionalDependencies: - '@parcel/watcher-android-arm64': 2.5.6 - '@parcel/watcher-darwin-arm64': 2.5.6 - '@parcel/watcher-darwin-x64': 2.5.6 - '@parcel/watcher-freebsd-x64': 2.5.6 - '@parcel/watcher-linux-arm-glibc': 2.5.6 - '@parcel/watcher-linux-arm-musl': 2.5.6 - '@parcel/watcher-linux-arm64-glibc': 2.5.6 - '@parcel/watcher-linux-arm64-musl': 2.5.6 - '@parcel/watcher-linux-x64-glibc': 2.5.6 - '@parcel/watcher-linux-x64-musl': 2.5.6 - '@parcel/watcher-win32-arm64': 2.5.6 - '@parcel/watcher-win32-ia32': 2.5.6 - '@parcel/watcher-win32-x64': 2.5.6 - - '@planetscale/database@1.19.0': {} - '@polka/url@1.0.0-next.29': {} '@posthog/core@1.28.3': @@ -11608,40 +9551,6 @@ snapshots: '@posthog/types@1.372.9': {} - '@prisma/adapter-planetscale@5.22.0(@planetscale/database@1.19.0)': - dependencies: - '@planetscale/database': 1.19.0 - '@prisma/driver-adapter-utils': 5.22.0 - - '@prisma/client@5.22.0(prisma@5.22.0)': - optionalDependencies: - prisma: 5.22.0 - - '@prisma/debug@5.22.0': {} - - '@prisma/driver-adapter-utils@5.22.0': - dependencies: - '@prisma/debug': 5.22.0 - - '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': {} - - '@prisma/engines@5.22.0': - dependencies: - '@prisma/debug': 5.22.0 - '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 - '@prisma/fetch-engine': 5.22.0 - '@prisma/get-platform': 5.22.0 - - '@prisma/fetch-engine@5.22.0': - dependencies: - '@prisma/debug': 5.22.0 - '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 - '@prisma/get-platform': 5.22.0 - - '@prisma/get-platform@5.22.0': - dependencies: - '@prisma/debug': 5.22.0 - '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -12447,66 +10356,59 @@ snapshots: dependencies: react: 19.2.3 - '@rolldown/binding-android-arm64@1.0.3': + '@rolldown/binding-android-arm64@1.1.0': optional: true - '@rolldown/binding-darwin-arm64@1.0.3': + '@rolldown/binding-darwin-arm64@1.1.0': optional: true - '@rolldown/binding-darwin-x64@1.0.3': + '@rolldown/binding-darwin-x64@1.1.0': optional: true - '@rolldown/binding-freebsd-x64@1.0.3': + '@rolldown/binding-freebsd-x64@1.1.0': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + '@rolldown/binding-linux-arm-gnueabihf@1.1.0': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.3': + '@rolldown/binding-linux-arm64-gnu@1.1.0': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.3': + '@rolldown/binding-linux-arm64-musl@1.1.0': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.3': + '@rolldown/binding-linux-ppc64-gnu@1.1.0': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.3': + '@rolldown/binding-linux-s390x-gnu@1.1.0': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.3': + '@rolldown/binding-linux-x64-gnu@1.1.0': optional: true - '@rolldown/binding-linux-x64-musl@1.0.3': + '@rolldown/binding-linux-x64-musl@1.1.0': optional: true - '@rolldown/binding-openharmony-arm64@1.0.3': + '@rolldown/binding-openharmony-arm64@1.1.0': optional: true - '@rolldown/binding-wasm32-wasi@1.0.3': + '@rolldown/binding-wasm32-wasi@1.1.0': dependencies: '@emnapi/core': 1.10.0 '@emnapi/runtime': 1.10.0 '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.3': + '@rolldown/binding-win32-arm64-msvc@1.1.0': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.3': + '@rolldown/binding-win32-x64-msvc@1.1.0': optional: true '@rolldown/pluginutils@1.0.0': {} '@rolldown/pluginutils@1.0.0-beta.27': {} - '@rollup/plugin-replace@6.0.3(rollup@4.55.1)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.55.1) - magic-string: 0.30.21 - optionalDependencies: - rollup: 4.55.1 - '@rollup/pluginutils@5.3.0(rollup@4.55.1)': dependencies: '@types/estree': 1.0.8 @@ -12775,19 +10677,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@t3-oss/env-core@0.10.1(typescript@5.9.3)(zod@4.3.5)': - dependencies: - zod: 4.3.5 - optionalDependencies: - typescript: 5.9.3 - - '@t3-oss/env-nextjs@0.10.1(typescript@5.9.3)(zod@4.3.5)': - dependencies: - '@t3-oss/env-core': 0.10.1(typescript@5.9.3)(zod@4.3.5) - zod: 4.3.5 - optionalDependencies: - typescript: 5.9.3 - '@tabler/icons-react@3.36.1(react@19.2.3)': dependencies: '@tabler/icons': 3.36.1 @@ -13055,31 +10944,6 @@ snapshots: '@textlint/utils@15.5.2': {} - '@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441)': - dependencies: - '@trpc/server': 11.0.0-rc.441 - - '@trpc/next@11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/react-query@11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/server@11.0.0-rc.441)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@trpc/server@11.0.0-rc.441)(next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@trpc/client': 11.0.0-rc.441(@trpc/server@11.0.0-rc.441) - '@trpc/server': 11.0.0-rc.441 - next: 16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - optionalDependencies: - '@tanstack/react-query': 5.90.16(react@19.2.3) - '@trpc/react-query': 11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/server@11.0.0-rc.441)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - - '@trpc/react-query@11.0.0-rc.441(@tanstack/react-query@5.90.16(react@19.2.3))(@trpc/client@11.0.0-rc.441(@trpc/server@11.0.0-rc.441))(@trpc/server@11.0.0-rc.441)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@tanstack/react-query': 5.90.16(react@19.2.3) - '@trpc/client': 11.0.0-rc.441(@trpc/server@11.0.0-rc.441) - '@trpc/server': 11.0.0-rc.441 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - - '@trpc/server@11.0.0-rc.441': {} - '@trpc/server@11.8.1(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -13128,12 +10992,6 @@ snapshots: '@types/argparse@1.0.38': {} - '@types/axios@0.14.4': - dependencies: - axios: 1.13.2 - transitivePeerDependencies: - - debug - '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.5 @@ -13160,8 +11018,6 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/cookie@0.6.0': {} - '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -13181,15 +11037,6 @@ snapshots: '@types/jsonfile': 6.1.4 '@types/node': 25.0.6 - '@types/glob@8.1.0': - dependencies: - '@types/minimatch': 5.1.2 - '@types/node': 22.19.5 - - '@types/gradient-string@1.1.6': - dependencies: - '@types/tinycolor2': 1.4.6 - '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -13223,8 +11070,6 @@ snapshots: '@types/mdx@2.0.13': {} - '@types/minimatch@5.1.2': {} - '@types/ms@2.1.0': {} '@types/node@12.20.55': {} @@ -13248,8 +11093,6 @@ snapshots: '@types/node': 25.0.6 kleur: 3.0.3 - '@types/randomstring@1.3.0': {} - '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: '@types/react': 19.2.7 @@ -13264,8 +11107,6 @@ snapshots: '@types/statuses@2.0.6': {} - '@types/tinycolor2@1.4.6': {} - '@types/trusted-types@2.0.7': optional: true @@ -13337,24 +11178,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@2.1.9(vitest@4.0.17(@opentelemetry/api@1.9.1)(@types/node@22.19.5)(happy-dom@20.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(msw@2.12.7(@types/node@22.19.5)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2))': - dependencies: - '@ampproject/remapping': 2.3.0 - '@bcoe/v8-coverage': 0.2.3 - debug: 4.4.3(supports-color@5.5.0) - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 - istanbul-reports: 3.2.0 - magic-string: 0.30.21 - magicast: 0.3.5 - std-env: 3.10.0 - test-exclude: 7.0.1 - tinyrainbow: 1.2.0 - vitest: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@22.19.5)(happy-dom@20.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(msw@2.12.7(@types/node@22.19.5)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) - transitivePeerDependencies: - - supports-color - '@vitest/coverage-v8@2.1.9(vitest@4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(happy-dom@20.1.0)(jiti@1.21.7)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 @@ -13637,18 +11460,6 @@ snapshots: astring@1.9.0: {} - asynckit@0.4.0: {} - - aws-ssl-profiles@1.1.2: {} - - axios@1.13.2: - dependencies: - follow-redirects: 1.15.11 - form-data: 4.0.5 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - bail@1.0.5: {} bail@2.0.2: {} @@ -13666,14 +11477,14 @@ snapshots: elkjs: 0.11.1 entities: 7.0.1 - better-auth@1.5.5(@prisma/client@5.22.0(prisma@5.22.0))(mongodb@7.1.0)(mysql2@3.16.0)(next@16.2.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(prisma@5.22.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@4.0.17): + better-auth@1.5.5(mongodb@7.1.0)(next@16.2.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vitest@4.0.17): dependencies: '@better-auth/core': 1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1) '@better-auth/drizzle-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1) '@better-auth/kysely-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.12) '@better-auth/memory-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1) '@better-auth/mongo-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0) - '@better-auth/prisma-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@5.22.0(prisma@5.22.0))(prisma@5.22.0) + '@better-auth/prisma-adapter': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1))(@better-auth/utils@0.3.1) '@better-auth/telemetry': 1.5.5(@better-auth/core@1.5.5(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.12)(nanostores@1.1.1)) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -13686,11 +11497,8 @@ snapshots: nanostores: 1.1.1 zod: 4.3.6 optionalDependencies: - '@prisma/client': 5.22.0(prisma@5.22.0) mongodb: 7.1.0 - mysql2: 3.16.0 next: 16.2.6(@babel/core@7.28.5)(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0) - prisma: 5.22.0 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) vitest: 4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(@vitest/ui@3.2.4)(happy-dom@20.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) @@ -13759,8 +11567,6 @@ snapshots: bson@7.2.0: {} - buffer-from@1.1.2: {} - buffer@4.9.2: dependencies: base64-js: 1.5.1 @@ -13893,14 +11699,6 @@ snapshots: dependencies: clsx: 2.1.1 - cli-color@2.0.4: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - es6-iterator: 2.0.3 - memoizee: 0.4.17 - timers-ext: 0.1.8 - cli-cursor@4.0.0: dependencies: restore-cursor: 4.0.0 @@ -13989,18 +11787,12 @@ snapshots: colorette@2.0.20: {} - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - comma-separated-tokens@2.0.3: {} commander@10.0.1: {} commander@14.0.2: {} - commander@9.5.0: {} - compare-versions@6.1.1: {} compute-scroll-into-view@3.1.1: {} @@ -14035,16 +11827,10 @@ snapshots: cookie-signature@1.2.2: {} - cookie@0.6.0: {} - cookie@0.7.2: {} cookie@1.1.1: {} - copy-anything@4.0.5: - dependencies: - is-what: 5.5.0 - core-js@3.49.0: {} cors@2.8.5: @@ -14073,11 +11859,6 @@ snapshots: csstype@3.2.3: {} - d@1.0.2: - dependencies: - es5-ext: 0.10.64 - type: 2.7.3 - data-uri-to-buffer@4.0.1: {} date-fns@2.30.0: @@ -14117,10 +11898,6 @@ snapshots: defu@6.1.4: {} - delayed-stream@1.0.0: {} - - denque@2.1.0: {} - depd@2.0.0: {} dequal@2.0.3: {} @@ -14129,8 +11906,6 @@ snapshots: detect-indent@6.1.0: {} - detect-libc@2.0.2: {} - detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -14147,10 +11922,6 @@ snapshots: diff@8.0.3: {} - difflib@0.2.4: - dependencies: - heap: 0.2.7 - dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -14163,35 +11934,6 @@ snapshots: dotenv@17.2.3: {} - dreamopt@0.8.0: - dependencies: - wordwrap: 1.0.0 - - drizzle-kit@0.21.4: - dependencies: - '@esbuild-kit/esm-loader': 2.6.5 - commander: 9.5.0 - env-paths: 3.0.0 - esbuild: 0.19.12 - esbuild-register: 3.6.0(esbuild@0.19.12) - glob: 11.1.0 - hanji: 0.0.5 - json-diff: 0.9.0 - zod: 3.25.76 - transitivePeerDependencies: - - supports-color - - drizzle-orm@0.30.10(@libsql/client@0.6.2)(@opentelemetry/api@1.9.1)(@planetscale/database@1.19.0)(@types/react@19.2.7)(kysely@0.28.12)(mysql2@3.16.0)(postgres@3.4.8)(react@19.2.3): - optionalDependencies: - '@libsql/client': 0.6.2 - '@opentelemetry/api': 1.9.1 - '@planetscale/database': 1.19.0 - '@types/react': 19.2.7 - kysely: 0.28.12 - mysql2: 3.16.0 - postgres: 3.4.8 - react: 19.2.3 - dts-resolver@2.1.3(oxc-resolver@11.16.2): optionalDependencies: oxc-resolver: 11.16.2 @@ -14243,8 +11985,6 @@ snapshots: entities@7.0.1: {} - env-paths@3.0.0: {} - environment@1.1.0: {} error-ex@1.3.4: @@ -14261,40 +12001,8 @@ snapshots: dependencies: es-errors: 1.3.0 - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - es-toolkit@1.43.0: {} - es5-ext@0.10.64: - dependencies: - es6-iterator: 2.0.3 - es6-symbol: 3.1.4 - esniff: 2.0.1 - next-tick: 1.1.0 - - es6-iterator@2.0.3: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - es6-symbol: 3.1.4 - - es6-symbol@3.1.4: - dependencies: - d: 1.0.2 - ext: 1.7.0 - - es6-weak-map@2.0.3: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - es6-iterator: 2.0.3 - es6-symbol: 3.1.4 - esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -14309,64 +12017,6 @@ snapshots: esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 - esbuild-register@3.6.0(esbuild@0.19.12): - dependencies: - debug: 4.4.3(supports-color@5.5.0) - esbuild: 0.19.12 - transitivePeerDependencies: - - supports-color - - esbuild@0.18.20: - optionalDependencies: - '@esbuild/android-arm': 0.18.20 - '@esbuild/android-arm64': 0.18.20 - '@esbuild/android-x64': 0.18.20 - '@esbuild/darwin-arm64': 0.18.20 - '@esbuild/darwin-x64': 0.18.20 - '@esbuild/freebsd-arm64': 0.18.20 - '@esbuild/freebsd-x64': 0.18.20 - '@esbuild/linux-arm': 0.18.20 - '@esbuild/linux-arm64': 0.18.20 - '@esbuild/linux-ia32': 0.18.20 - '@esbuild/linux-loong64': 0.18.20 - '@esbuild/linux-mips64el': 0.18.20 - '@esbuild/linux-ppc64': 0.18.20 - '@esbuild/linux-riscv64': 0.18.20 - '@esbuild/linux-s390x': 0.18.20 - '@esbuild/linux-x64': 0.18.20 - '@esbuild/netbsd-x64': 0.18.20 - '@esbuild/openbsd-x64': 0.18.20 - '@esbuild/sunos-x64': 0.18.20 - '@esbuild/win32-arm64': 0.18.20 - '@esbuild/win32-ia32': 0.18.20 - '@esbuild/win32-x64': 0.18.20 - - esbuild@0.19.12: - optionalDependencies: - '@esbuild/aix-ppc64': 0.19.12 - '@esbuild/android-arm': 0.19.12 - '@esbuild/android-arm64': 0.19.12 - '@esbuild/android-x64': 0.19.12 - '@esbuild/darwin-arm64': 0.19.12 - '@esbuild/darwin-x64': 0.19.12 - '@esbuild/freebsd-arm64': 0.19.12 - '@esbuild/freebsd-x64': 0.19.12 - '@esbuild/linux-arm': 0.19.12 - '@esbuild/linux-arm64': 0.19.12 - '@esbuild/linux-ia32': 0.19.12 - '@esbuild/linux-loong64': 0.19.12 - '@esbuild/linux-mips64el': 0.19.12 - '@esbuild/linux-ppc64': 0.19.12 - '@esbuild/linux-riscv64': 0.19.12 - '@esbuild/linux-s390x': 0.19.12 - '@esbuild/linux-x64': 0.19.12 - '@esbuild/netbsd-x64': 0.19.12 - '@esbuild/openbsd-x64': 0.19.12 - '@esbuild/sunos-x64': 0.19.12 - '@esbuild/win32-arm64': 0.19.12 - '@esbuild/win32-ia32': 0.19.12 - '@esbuild/win32-x64': 0.19.12 - esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -14464,13 +12114,6 @@ snapshots: escape-string-regexp@5.0.0: {} - esniff@2.0.1: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - event-emitter: 0.3.5 - type: 2.7.3 - esprima@4.0.1: {} estree-util-attach-comments@3.0.0: @@ -14514,11 +12157,6 @@ snapshots: etag@1.8.1: {} - event-emitter@0.3.5: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - eventemitter3@5.0.1: {} eventsource-parser@3.0.6: {} @@ -14603,10 +12241,6 @@ snapshots: exsolve@1.0.8: {} - ext@1.7.0: - dependencies: - type: 2.7.3 - extend@3.0.2: {} extendable-error@0.1.7: {} @@ -14631,18 +12265,8 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-string-truncated-width@3.0.3: {} - - fast-string-width@3.0.2: - dependencies: - fast-string-truncated-width: 3.0.3 - fast-uri@3.1.0: {} - fast-wrap-ansi@0.2.0: - dependencies: - fast-string-width: 3.0.2 - fast-xml-parser@5.3.3: dependencies: strnum: 2.1.2 @@ -14695,8 +12319,6 @@ snapshots: transitivePeerDependencies: - supports-color - find-my-way-ts@0.1.6: {} - find-up-simple@1.0.1: {} find-up@3.0.0: @@ -14718,21 +12340,11 @@ snapshots: flatted@3.4.2: {} - follow-redirects@1.15.11: {} - foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - format@0.2.2: {} formatly@0.3.0: @@ -14938,10 +12550,6 @@ snapshots: function-bind@1.1.2: {} - generate-function@2.3.1: - dependencies: - is-property: 1.0.2 - gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -15020,27 +12628,8 @@ snapshots: graceful-fs@4.2.11: {} - gradient-string@2.0.2: - dependencies: - chalk: 4.1.2 - tinygradient: 1.1.5 - graphql@16.12.0: {} - handlebars@4.7.8: - dependencies: - minimist: 1.2.8 - neo-async: 2.6.2 - source-map: 0.6.1 - wordwrap: 1.0.0 - optionalDependencies: - uglify-js: 3.19.3 - - hanji@0.0.5: - dependencies: - lodash.throttle: 4.1.1 - sisteransi: 1.0.5 - happy-dom@20.1.0: dependencies: '@types/node': 20.19.28 @@ -15058,10 +12647,6 @@ snapshots: has-symbols@1.1.0: {} - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - hashery@1.5.0: dependencies: hookified: 1.15.1 @@ -15137,8 +12722,6 @@ snapshots: headers-polyfill@4.0.3: {} - heap@0.2.7: {} - highlight.js@10.7.3: {} hono@4.11.3: {} @@ -15203,8 +12786,6 @@ snapshots: inherits@2.0.4: {} - ini@4.1.3: {} - inline-style-parser@0.2.7: {} ipaddr.js@1.9.1: {} @@ -15277,12 +12858,8 @@ snapshots: is-plain-obj@4.1.0: {} - is-promise@2.2.2: {} - is-promise@4.0.0: {} - is-property@1.0.2: {} - is-regexp@3.1.0: {} is-stream@3.0.0: {} @@ -15297,8 +12874,6 @@ snapshots: is-unicode-supported@2.1.0: {} - is-what@5.5.0: {} - is-windows@1.0.2: {} is-wsl@3.1.0: @@ -15384,14 +12959,8 @@ snapshots: jju@1.4.0: {} - jose@4.15.9: {} - - jose@5.10.0: {} - jose@6.1.3: {} - js-base64@3.7.8: {} - js-cookie@2.2.1: {} js-tokens@4.0.0: {} @@ -15407,12 +12976,6 @@ snapshots: jsesc@3.1.0: {} - json-diff@0.9.0: - dependencies: - cli-color: 2.0.4 - difflib: 0.2.4 - dreamopt: 0.8.0 - json-parse-even-better-errors@2.3.1: {} json-schema-traverse@1.0.0: {} @@ -15460,8 +13023,6 @@ snapshots: kolorist@1.8.0: {} - kubernetes-types@1.30.0: {} - kysely@0.28.12: {} levn@0.4.1: @@ -15469,19 +13030,6 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libsql@0.3.19: - dependencies: - '@neon-rs/load': 0.0.4 - detect-libc: 2.0.2 - optionalDependencies: - '@libsql/darwin-arm64': 0.3.19 - '@libsql/darwin-x64': 0.3.19 - '@libsql/linux-arm64-gnu': 0.3.19 - '@libsql/linux-arm64-musl': 0.3.19 - '@libsql/linux-x64-gnu': 0.3.19 - '@libsql/linux-x64-musl': 0.3.19 - '@libsql/win32-x64-msvc': 0.3.19 - lightningcss-android-arm64@1.30.2: optional: true @@ -15617,12 +13165,6 @@ snapshots: dependencies: yallist: 4.0.0 - lru-queue@0.1.0: - dependencies: - es5-ext: 0.10.64 - - lru.min@1.1.3: {} - lucide-react@0.511.0(react@19.2.3): dependencies: react: 19.2.3 @@ -15908,17 +13450,6 @@ snapshots: media-typer@1.1.0: {} - memoizee@0.4.17: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - es6-weak-map: 2.0.3 - event-emitter: 0.3.5 - is-promise: 2.2.2 - lru-queue: 0.1.0 - next-tick: 1.1.0 - timers-ext: 0.1.8 - memory-pager@1.5.0: {} merge-descriptors@2.0.0: {} @@ -16250,20 +13781,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mime-db@1.52.0: {} - mime-db@1.54.0: {} - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - mime-types@3.0.2: dependencies: mime-db: 1.54.0 - mime@3.0.0: {} - mimic-fn@2.1.0: {} mimic-fn@4.0.0: {} @@ -16324,22 +13847,6 @@ snapshots: ms@2.1.3: {} - msgpackr-extract@3.0.3: - dependencies: - node-gyp-build-optional-packages: 5.2.2 - optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 - optional: true - - msgpackr@1.11.9: - optionalDependencies: - msgpackr-extract: 3.0.3 - msw@2.12.7(@types/node@22.19.5)(typescript@5.9.3): dependencies: '@inquirer/confirm': 5.1.21(@types/node@22.19.5) @@ -16393,34 +13900,14 @@ snapshots: muggle-string@0.4.1: {} - multipasta@0.2.7: {} - mute-stream@2.0.0: {} - mute-stream@3.0.0: {} - - mysql2@3.16.0: - dependencies: - aws-ssl-profiles: 1.1.2 - denque: 2.1.0 - generate-function: 2.3.1 - iconv-lite: 0.7.2 - long: 5.3.2 - lru.min: 1.1.3 - named-placeholders: 1.1.6 - seq-queue: 0.0.5 - sqlstring: 2.3.3 - mz@2.7.0: dependencies: any-promise: 1.3.0 object-assign: 4.1.1 thenify-all: 1.6.0 - named-placeholders@1.1.6: - dependencies: - lru.min: 1.1.3 - nano-spawn@2.0.0: {} nanoid@3.3.11: {} @@ -16429,32 +13916,13 @@ snapshots: negotiator@1.0.0: {} - neo-async@2.6.2: {} - neotraverse@0.6.18: {} - next-auth@4.24.13(next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): - dependencies: - '@babel/runtime': 7.28.4 - '@panva/hkdf': 1.2.1 - cookie: 0.7.2 - jose: 4.15.9 - next: 16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0) - oauth: 0.9.15 - openid-client: 5.7.1 - preact: 10.28.2 - preact-render-to-string: 5.2.6(preact@10.28.2) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - uuid: 8.3.2 - next-themes@0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - next-tick@1.1.0: {} - next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0): dependencies: '@next/env': '@varlock/nextjs-integration@1.1.0(next@16.1.1(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(varlock@1.2.0))(varlock@1.2.0)' @@ -16507,8 +13975,6 @@ snapshots: - babel-plugin-macros - varlock - node-addon-api@7.1.1: {} - node-domexception@1.0.0: {} node-emoji@2.2.0: @@ -16530,11 +13996,6 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - node-gyp-build-optional-packages@5.2.2: - dependencies: - detect-libc: 2.1.2 - optional: true - node-releases@2.0.27: {} nodemon@3.1.11: @@ -16577,16 +14038,8 @@ snapshots: pkg-types: 2.3.0 tinyexec: 1.0.2 - oauth4webapi@2.17.0: {} - - oauth4webapi@3.8.3: {} - - oauth@0.9.15: {} - object-assign@4.1.1: {} - object-hash@2.2.0: {} - object-inspect@1.13.4: {} obug@2.1.1: {} @@ -16599,8 +14052,6 @@ snapshots: ohash@2.0.11: {} - oidc-token-hash@5.2.0: {} - on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -16636,13 +14087,6 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 - openid-client@5.7.1: - dependencies: - jose: 4.15.9 - lru-cache: 6.0.0 - object-hash: 2.2.0 - oidc-token-hash: 5.2.0 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -16850,8 +14294,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postgres@3.4.8: {} - posthog-js@1.372.9: dependencies: '@opentelemetry/api': 1.9.1 @@ -16868,20 +14310,6 @@ snapshots: query-selector-shadow-dom: 1.0.1 web-vitals: 5.2.0 - preact-render-to-string@5.2.3(preact@10.28.2): - dependencies: - preact: 10.28.2 - pretty-format: 3.8.0 - - preact-render-to-string@5.2.6(preact@10.28.2): - dependencies: - preact: 10.28.2 - pretty-format: 3.8.0 - - preact-render-to-string@6.5.11(preact@10.28.2): - dependencies: - preact: 10.28.2 - preact@10.28.2: {} prelude-ls@1.2.1: {} @@ -16896,18 +14324,10 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - pretty-format@3.8.0: {} - pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 - prisma@5.22.0: - dependencies: - '@prisma/engines': 5.22.0 - optionalDependencies: - fsevents: 2.3.3 - prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -16935,8 +14355,6 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - proxy-from-env@1.1.0: {} - pstree.remy@1.1.8: {} publint@0.3.16: @@ -17029,14 +14447,6 @@ snapshots: '@types/react': 19.2.7 '@types/react-dom': 19.2.3(@types/react@19.2.7) - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - - randomstring@1.3.1: - dependencies: - randombytes: 2.1.0 - range-parser@1.2.1: {} raw-body@3.0.2: @@ -17310,7 +14720,7 @@ snapshots: rfdc@1.4.1: {} - rolldown-plugin-dts@0.15.10(oxc-resolver@11.16.2)(rolldown@1.0.3)(typescript@5.9.3): + rolldown-plugin-dts@0.15.10(oxc-resolver@11.16.2)(rolldown@1.1.0)(typescript@5.9.3): dependencies: '@babel/generator': 7.28.5 '@babel/parser': 7.28.5 @@ -17320,33 +14730,33 @@ snapshots: debug: 4.4.3(supports-color@5.5.0) dts-resolver: 2.1.3(oxc-resolver@11.16.2) get-tsconfig: 4.13.0 - rolldown: 1.0.3 + rolldown: 1.1.0 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - oxc-resolver - supports-color - rolldown@1.0.3: + rolldown@1.1.0: dependencies: - '@oxc-project/types': 0.133.0 + '@oxc-project/types': 0.134.0 '@rolldown/pluginutils': 1.0.0 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.3 - '@rolldown/binding-darwin-arm64': 1.0.3 - '@rolldown/binding-darwin-x64': 1.0.3 - '@rolldown/binding-freebsd-x64': 1.0.3 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 - '@rolldown/binding-linux-arm64-gnu': 1.0.3 - '@rolldown/binding-linux-arm64-musl': 1.0.3 - '@rolldown/binding-linux-ppc64-gnu': 1.0.3 - '@rolldown/binding-linux-s390x-gnu': 1.0.3 - '@rolldown/binding-linux-x64-gnu': 1.0.3 - '@rolldown/binding-linux-x64-musl': 1.0.3 - '@rolldown/binding-openharmony-arm64': 1.0.3 - '@rolldown/binding-wasm32-wasi': 1.0.3 - '@rolldown/binding-win32-arm64-msvc': 1.0.3 - '@rolldown/binding-win32-x64-msvc': 1.0.3 + '@rolldown/binding-android-arm64': 1.1.0 + '@rolldown/binding-darwin-arm64': 1.1.0 + '@rolldown/binding-darwin-x64': 1.1.0 + '@rolldown/binding-freebsd-x64': 1.1.0 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.0 + '@rolldown/binding-linux-arm64-gnu': 1.1.0 + '@rolldown/binding-linux-arm64-musl': 1.1.0 + '@rolldown/binding-linux-ppc64-gnu': 1.1.0 + '@rolldown/binding-linux-s390x-gnu': 1.1.0 + '@rolldown/binding-linux-x64-gnu': 1.1.0 + '@rolldown/binding-linux-x64-musl': 1.1.0 + '@rolldown/binding-openharmony-arm64': 1.1.0 + '@rolldown/binding-wasm32-wasi': 1.1.0 + '@rolldown/binding-win32-arm64-msvc': 1.1.0 + '@rolldown/binding-win32-x64-msvc': 1.1.0 rollup-plugin-preserve-directives@0.4.0(rollup@4.55.1): dependencies: @@ -17445,8 +14855,6 @@ snapshots: transitivePeerDependencies: - supports-color - seq-queue@0.0.5: {} - serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -17619,11 +15027,6 @@ snapshots: source-map-js@1.2.1: {} - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - source-map@0.6.1: {} source-map@0.7.6: {} @@ -17657,8 +15060,6 @@ snapshots: sprintf-js@1.0.3: {} - sqlstring@2.3.3: {} - stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -17764,10 +15165,6 @@ snapshots: optionalDependencies: '@babel/core': 7.28.5 - superjson@2.2.6: - dependencies: - copy-anything: 4.0.5 - supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -17854,17 +15251,10 @@ snapshots: dependencies: any-promise: 1.3.0 - timers-ext@0.1.8: - dependencies: - es5-ext: 0.10.64 - next-tick: 1.1.0 - tiny-invariant@1.3.3: {} tinybench@2.9.0: {} - tinycolor2@1.6.0: {} - tinyexec@1.0.2: {} tinyglobby@0.2.15: @@ -17872,11 +15262,6 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tinygradient@1.1.5: - dependencies: - '@types/tinycolor2': 1.4.6 - tinycolor2: 1.6.0 - tinyrainbow@1.2.0: {} tinyrainbow@2.0.0: {} @@ -17895,8 +15280,6 @@ snapshots: toidentifier@1.0.1: {} - toml@3.0.0: {} - totalist@3.0.1: {} touch@3.1.1: {} @@ -17963,8 +15346,8 @@ snapshots: diff: 8.0.2 empathic: 2.0.0 hookable: 5.5.3 - rolldown: 1.0.3 - rolldown-plugin-dts: 0.15.10(oxc-resolver@11.16.2)(rolldown@1.0.3)(typescript@5.9.3) + rolldown: 1.1.0 + rolldown-plugin-dts: 0.15.10(oxc-resolver@11.16.2)(rolldown@1.1.0)(typescript@5.9.3) semver: 7.7.3 tinyexec: 1.0.2 tinyglobby: 0.2.15 @@ -18029,8 +15412,6 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - type@2.7.3: {} - typescript@5.4.2: {} typescript@5.6.1-rc: {} @@ -18041,9 +15422,6 @@ snapshots: ufo@1.6.2: {} - uglify-js@3.19.3: - optional: true - ultracite@7.0.8(effect@3.20.0)(typescript@5.9.3): dependencies: '@clack/prompts': 0.11.0 @@ -18084,8 +15462,6 @@ snapshots: undici-types@7.16.0: {} - undici@7.24.4: {} - unfetch@4.2.0: {} unicode-emoji-modifier-base@1.0.0: {} @@ -18204,8 +15580,6 @@ snapshots: uuid@11.1.0: {} - uuid@8.3.2: {} - validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 @@ -18549,45 +15923,6 @@ snapshots: - tsx - yaml - vitest@4.0.17(@opentelemetry/api@1.9.1)(@types/node@25.0.6)(happy-dom@20.1.0)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2): - dependencies: - '@vitest/expect': 4.0.17 - '@vitest/mocker': 4.0.17(msw@2.12.7(@types/node@25.0.6)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.5)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) - '@vitest/pretty-format': 4.0.17 - '@vitest/runner': 4.0.17 - '@vitest/snapshot': 4.0.17 - '@vitest/spy': 4.0.17 - '@vitest/utils': 4.0.17 - es-module-lexer: 1.7.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 6.4.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) - why-is-node-running: 2.3.0 - optionalDependencies: - '@opentelemetry/api': 1.9.1 - '@types/node': 25.0.6 - happy-dom: 20.1.0 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - vscode-uri@3.1.0: {} walk-up-path@4.0.0: {} @@ -18629,8 +15964,6 @@ snapshots: word-wrap@1.2.5: {} - wordwrap@1.0.0: {} - wrap-ansi@5.1.0: dependencies: ansi-styles: 3.2.1