diff --git a/.changeset/npm-min-release-age.md b/.changeset/npm-min-release-age.md new file mode 100644 index 00000000..fccada28 --- /dev/null +++ b/.changeset/npm-min-release-age.md @@ -0,0 +1,5 @@ +--- +"@proofkit/cli": patch +--- + +Write npm min-release-age config during npm project scaffolding. diff --git a/.changeset/ultracite-init.md b/.changeset/ultracite-init.md new file mode 100644 index 00000000..854e61b4 --- /dev/null +++ b/.changeset/ultracite-init.md @@ -0,0 +1,5 @@ +--- +"@proofkit/cli": patch +--- + +Run Ultracite and TanStack Intent setup during project scaffolding. diff --git a/packages/cli/package.json b/packages/cli/package.json index c89885ea..30469dc1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -38,7 +38,7 @@ "package.json" ], "engines": { - "node": "^20.0.0 || ^22.0.0" + "node": "^22.0.0 || ^24.0.0 || ^26.0.0" }, "scripts": { "typecheck": "node ./scripts/write-cli-version.mjs && tsc", diff --git a/packages/cli/src/cli/init.ts b/packages/cli/src/cli/init.ts index f4cd1f54..0d4ceee4 100644 --- a/packages/cli/src/cli/init.ts +++ b/packages/cli/src/cli/init.ts @@ -5,20 +5,24 @@ import fs from "fs-extra"; import type { PackageJson } from "type-fest"; import { DEFAULT_APP_NAME } 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, getUltraciteInitCommand } 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 { parseNameAndPath } from "~/utils/parseNameAndPath.js"; import { type Settings, setSettings } from "~/utils/parseSettings.js"; +import { formatPackageManagerCommand } from "~/utils/projectFiles.js"; import { validateAppName } from "~/utils/validateAppName.js"; import { promptForFileMakerDataSource } from "./add/data-source/filemaker.js"; import { select, text } from "./prompts.js"; @@ -132,9 +136,19 @@ type ProofKitPackageJSON = PackageJson & { version: string; onFail: "download"; }; + runtime: { + name: "node"; + version: string; + onFail: "download"; + }; + }; + engines?: { + node: string; }; }; +const NODE_RUNTIME_VERSION = "^24.11.0"; + const missingTypegenCommandPatterns = [ /ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL[\s\S]*Command\s+["'`]typegen["'`]\s+not found/i, /Command\s+["'`]typegen["'`]\s+not found/i, @@ -289,23 +303,40 @@ export const runInit = async (name?: string, opts?: CliFlags) => { 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: stdout.trim(), + 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 = { @@ -366,6 +397,24 @@ export const runInit = async (name?: string, opts?: CliFlags) => { await installDependencies({ projectDir }); } + const ultraciteCommand = getUltraciteInitCommand({ + appType: state.appType ?? "browser", + packageManager: pkgManager, + skipInstall: noInstall, + }); + await execa(ultraciteCommand.command, ultraciteCommand.args, { + cwd: projectDir, + stdio: "pipe", + }); + if ((state.appType ?? "browser") === "browser") { + fs.writeFileSync(path.join(projectDir, "oxlint.config.ts"), getBrowserOxlintConfig(), "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); @@ -382,6 +431,26 @@ export const runInit = async (name?: string, opts?: CliFlags) => { } } + if (!noInstall) { + const [fixCommand, ...fixArgs] = formatPackageManagerCommand(pkgManager, "fix").split(" "); + if (!fixCommand) { + throw new Error(`Unable to resolve fix command for ${pkgManager}.`); + } + await execa(fixCommand, fixArgs, { + cwd: projectDir, + stdio: "pipe", + }).catch(() => undefined); + + const [lintCommand, ...lintArgs] = formatPackageManagerCommand(pkgManager, "lint").split(" "); + if (!lintCommand) { + throw new Error(`Unable to resolve lint command for ${pkgManager}.`); + } + await execa(lintCommand, lintArgs, { + cwd: projectDir, + stdio: "pipe", + }); + } + if (!noGit) { await initializeGit(projectDir); } diff --git a/packages/cli/src/consts.ts b/packages/cli/src/consts.ts index be8a4ef5..fe357c5a 100644 --- a/packages/cli/src/consts.ts +++ b/packages/cli/src/consts.ts @@ -11,10 +11,9 @@ export const cliName = "proofkit"; export const npmName = "@proofkit/cli"; export const DOCS_URL = "https://proofkit.proof.sh"; -export const AGENT_INSTRUCTIONS = [ - "Use the ProofKit docs as the primary reference for this project: https://proofkit.proof.sh/docs", - "Before doing any AI-assisted development here, run `npx @tanstack/intent@latest install` in the project root to load skills relevant to this project", -].join("\n"); +export function getAgentInstructions() { + return "Use the ProofKit docs as the primary reference for this project: https://proofkit.proof.sh/docs"; +} // Registry URL is injected at build time via tsdown define. declare const __REGISTRY_URL__: string; diff --git a/packages/cli/src/core/executeInitPlan.ts b/packages/cli/src/core/executeInitPlan.ts index 3529f369..e02b90a8 100644 --- a/packages/cli/src/core/executeInitPlan.ts +++ b/packages/cli/src/core/executeInitPlan.ts @@ -3,7 +3,7 @@ import { Chalk } from "chalk"; import { Cause, Effect, Exit } from "effect"; import { getOrUndefined } from "effect/Option"; -import { AGENT_INSTRUCTIONS } from "~/consts.js"; +import { getAgentInstructions } from "~/consts.js"; import { CliContext, CodegenService, @@ -19,7 +19,14 @@ import { import { DirectoryConflictError, FileSystemError, isCliError, UserCancelledError } from "~/core/errors.js"; import { applyPackageJsonMutations } from "~/core/planInit.js"; import type { InitPlan } from "~/core/types.js"; -import { normalizeImportAlias, replaceTextInFiles, updateTypegenConfig } from "~/utils/projectFiles.js"; +import { getIntentInstallCommand } from "~/helpers/intent.js"; +import { getBrowserOxlintConfig, getUltraciteInitCommand } from "~/helpers/ultracite.js"; +import { + formatPackageManagerCommand, + normalizeImportAlias, + replaceTextInFiles, + updateTypegenConfig, +} from "~/utils/projectFiles.js"; import { sortPackageJson } from "~/utils/sortPackageJson.js"; const AGENT_METADATA_DIRS = new Set([".agents", ".claude", ".clawed", ".clinerules", ".cursor", ".windsurf"]); @@ -78,6 +85,14 @@ function renderNextSteps(plan: InitPlan, additionalSteps: string[] = []) { return lines.join("\n"); } +function getPackageScriptCommand(plan: InitPlan, scriptName: string) { + const [command, ...args] = formatPackageManagerCommand(plan.request.packageManager, scriptName).split(" "); + 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)) { @@ -275,7 +290,7 @@ export const executeInitPlan = (plan: InitPlan) => }), }); yield* Effect.tryPromise({ - try: () => replaceTextInFiles(projectFilesFs, plan.targetDir, "__AGENT_INSTRUCTIONS__", AGENT_INSTRUCTIONS), + try: () => replaceTextInFiles(projectFilesFs, plan.targetDir, "__AGENT_INSTRUCTIONS__", getAgentInstructions()), catch: (cause) => new FileSystemError({ message: "Unable to rewrite scaffold placeholders.", @@ -380,14 +395,6 @@ export const executeInitPlan = (plan: InitPlan) => } if (plan.tasks.runInstall) { - if (plan.request.packageManager === "pnpm") { - yield* processService.run("pnpm", ["self-update", "11"], { - cwd: plan.targetDir, - stdout: "pipe", - stderr: "pipe", - }); - } - let installArgs: string[] = ["install"]; if (plan.request.packageManager === "yarn") { installArgs = []; @@ -399,10 +406,61 @@ export const executeInitPlan = (plan: InitPlan) => }); } + if (plan.tasks.runUltraciteInit) { + 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", + }); + + if (plan.request.appType === "browser") { + yield* fs.writeFile(path.join(plan.targetDir, "oxlint.config.ts"), getBrowserOxlintConfig()); + } + } + + 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); } + 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 fixCommand = getPackageScriptCommand(plan, "fix"); + const result = yield* Effect.either( + processService.run(fixCommand.command, fixCommand.args, { + cwd: plan.targetDir, + stdout: "pipe", + stderr: "pipe", + }), + ); + if (result._tag === "Left") { + consoleService.warn("Lint fix did not succeed; continuing setup."); + } + } + if (plan.tasks.initializeGit) { yield* gitService.initialize(plan.targetDir); } diff --git a/packages/cli/src/core/planInit.ts b/packages/cli/src/core/planInit.ts index cf283c5f..f69fb5a7 100644 --- a/packages/cli/src/core/planInit.ts +++ b/packages/cli/src/core/planInit.ts @@ -17,34 +17,30 @@ import { } from "~/utils/projectFiles.js"; import { getNodeMajorVersion } from "~/utils/versioning.js"; -const PNPM_BUILD_POLICY = { +const SHARED_PNPM_BUILD_POLICY = { "@parcel/watcher": true, esbuild: true, "msgpackr-extract": true, msw: true, - sharp: 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."; -function getPackageManagerMajorVersion(version?: string) { - if (!version) { - return undefined; - } +const NODE_RUNTIME_VERSION = "^24.11.0"; +const NPM_MIN_RELEASE_AGE_DAYS = 1; - const major = Number.parseInt(version.split(".")[0] ?? "", 10); - return Number.isFinite(major) ? major : undefined; -} +export function createPnpmWorkspaceFileContent(appType: InitRequest["appType"]) { + const buildPolicy = { + ...SHARED_PNPM_BUILD_POLICY, + sharp: appType === "browser", + } as const; -function createPnpmWorkspaceFileContent() { 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(PNPM_BUILD_POLICY).map( - ([packageName, allowed]) => ` ${JSON.stringify(packageName)}: ${allowed}`, - ), + ...Object.entries(buildPolicy).map(([packageName, allowed]) => ` ${JSON.stringify(packageName)}: ${allowed}`), "", "trustPolicy: no-downgrade", "", @@ -55,6 +51,14 @@ function createPnpmWorkspaceFileContent() { ].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, @@ -74,7 +78,7 @@ const sharedUiDependencies = { "@radix-ui/react-slot": "^1.2.3", "class-variance-authority": "^0.7.1", clsx: "^2.1.1", - "lucide-react": "^0.577.0", + "lucide-react": "^1.16.0", "tailwind-merge": "^3.5.0", tailwindcss: "^4.1.10", "tw-animate-css": "^1.4.0", @@ -92,11 +96,14 @@ export function planInit( const settings = createDefaultSettings(request); const packageManagerCommand = getTemplatePackageCommand(request.packageManager); const packageManagerExecuteCommand = getTemplatePackageExecuteCommand(request.packageManager); - const packageManagerMajorVersion = getPackageManagerMajorVersion(options.packageManagerVersion); - const shouldWritePnpmWorkspaceFile = request.packageManager === "pnpm" && (packageManagerMajorVersion ?? 0) >= 11; + 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: { @@ -104,6 +111,11 @@ export function planInit( version: options.packageManagerVersion, onFail: "download", }, + runtime: { + name: "node", + version: NODE_RUNTIME_VERSION, + onFail: "download", + }, } : undefined, proofkitMetadata: { @@ -167,21 +179,37 @@ export function planInit( ? [ { path: path.join(targetDir, "pnpm-workspace.yaml"), - content: createPnpmWorkspaceFileContent(), + content: createPnpmWorkspaceFileContent(request.appType), + }, + ] + : []), + ...(shouldWriteNpmrcFile + ? [ + { + path: path.join(targetDir, ".npmrc"), + content: createNpmrcFileContent(), }, ] : []), ], commands: [ ...(request.noInstall ? [] : [{ type: "install" as const }]), + { type: "ultracite-init" as const }, + { 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: true, runInitialCodegen: shouldRunInitialCodegen, + runFix: !request.noInstall, + runLint: !request.noInstall, initializeGit: !request.noGit, }, nextSteps: [ @@ -212,6 +240,7 @@ export function applyPackageJsonMutations( packageJson.devEngines = mutations.devEngines; packageJson.packageManager = undefined; } + packageJson.engines = mutations.engines as PackageJson["engines"]; if (!packageJson.dependencies) { packageJson.dependencies = {}; diff --git a/packages/cli/src/core/types.ts b/packages/cli/src/core/types.ts index bb8fb5a5..4bacf384 100644 --- a/packages/cli/src/core/types.ts +++ b/packages/cli/src/core/types.ts @@ -111,6 +111,14 @@ export interface InitPlan { version: string; onFail: "download"; }; + runtime: { + name: "node"; + version: string; + onFail: "download"; + }; + }; + engines: { + node: string; }; proofkitMetadata: { initVersion: string; @@ -128,12 +136,24 @@ export interface InitPlan { path: string; content: string; }>; - commands: Array<{ type: "install" } | { type: "codegen" } | { type: "git-init" }>; + 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[]; diff --git a/packages/cli/src/helpers/createProject.ts b/packages/cli/src/helpers/createProject.ts index 63609d00..99ccce66 100644 --- a/packages/cli/src/helpers/createProject.ts +++ b/packages/cli/src/helpers/createProject.ts @@ -1,6 +1,6 @@ import path from "node:path"; -import { AGENT_INSTRUCTIONS } from "~/consts.js"; +import { getAgentInstructions } from "~/consts.js"; import { installPackages } from "~/helpers/installPackages.js"; import { scaffoldProject } from "~/helpers/scaffoldProject.js"; import type { AvailableDependencies } from "~/installers/dependencyVersionMap.js"; @@ -106,7 +106,7 @@ export const createBareProject = async ({ replaceTextInFiles(state.projectDir, "__PNPM_COMMAND__", pkgManagerCommand); replaceTextInFiles(state.projectDir, "__PACKAGE_MANAGER__", pkgManager); - replaceTextInFiles(state.projectDir, "__AGENT_INSTRUCTIONS__", AGENT_INSTRUCTIONS); + replaceTextInFiles(state.projectDir, "__AGENT_INSTRUCTIONS__", getAgentInstructions()); return state.projectDir; }; diff --git a/packages/cli/src/helpers/intent.ts b/packages/cli/src/helpers/intent.ts new file mode 100644 index 00000000..ce8a9588 --- /dev/null +++ b/packages/cli/src/helpers/intent.ts @@ -0,0 +1,18 @@ +import type { PackageManager } from "~/utils/packageManager.js"; +import { getTemplatePackageExecuteCommand } from "~/utils/projectFiles.js"; + +function splitExecuteCommand(packageManager: PackageManager) { + const [command, ...args] = getTemplatePackageExecuteCommand(packageManager).split(" "); + 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 index b4df3059..4dc251cb 100644 --- a/packages/cli/src/helpers/logNextSteps.ts +++ b/packages/cli/src/helpers/logNextSteps.ts @@ -5,6 +5,7 @@ import type { InstallerOptions } from "~/installers/index.js"; import { state } from "~/state.js"; import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; import { logger } from "~/utils/logger.js"; +import { getTemplatePackageExecuteCommand } from "~/utils/projectFiles.js"; const formatRunCommand = (pkgManager: ReturnType, command: string) => ["npm", "bun"].includes(pkgManager) ? `${pkgManager} run ${command}` : `${pkgManager} ${command}`; @@ -32,7 +33,7 @@ export const logNextSteps = ({ } logger.dim("\nAgent instruction (required before continuing any AI-assisted development in this project):"); - logger.info(" npx @tanstack/intent@latest install"); + logger.info(` ${getTemplatePackageExecuteCommand(pkgManager)} @tanstack/intent@latest install`); logger.dim( " Have your agent run this command in the newly created project so it can discover and learn important skills relevant to the project.", ); diff --git a/packages/cli/src/helpers/ultracite.ts b/packages/cli/src/helpers/ultracite.ts new file mode 100644 index 00000000..6ee96ea7 --- /dev/null +++ b/packages/cli/src/helpers/ultracite.ts @@ -0,0 +1,75 @@ +import type { AppType } from "~/core/types.js"; +import type { PackageManager } from "~/utils/packageManager.js"; +import { getTemplatePackageExecuteCommand } from "~/utils/projectFiles.js"; + +const ULTRACITE_EDITORS = ["universal", "cursor"] as const; +const ULTRACITE_AGENTS = ["universal", "claude", "codex"] as const; +const ULTRACITE_HOOKS = ["cursor", "windsurf", "codebuddy", "claude"] as const; +const ULTRACITE_INTEGRATIONS = ["husky", "lint-staged"] as const; + +function splitExecuteCommand(packageManager: PackageManager) { + const [command, ...args] = getTemplatePackageExecuteCommand(packageManager).split(" "); + 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", + "--quiet", + "--linter", + "oxlint", + "--pm", + packageManager, + "--frameworks", + ...getUltraciteFrameworks(appType), + "--editors", + ...ULTRACITE_EDITORS, + "--agents", + ...ULTRACITE_AGENTS, + "--hooks", + ...ULTRACITE_HOOKS, + "--integrations", + ...ULTRACITE_INTEGRATIONS, + ...(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}, +}); +`; +} diff --git a/packages/cli/src/installers/dependencyVersionMap.ts b/packages/cli/src/installers/dependencyVersionMap.ts index b2156fb2..ea4f4cef 100644 --- a/packages/cli/src/installers/dependencyVersionMap.ts +++ b/packages/cli/src/installers/dependencyVersionMap.ts @@ -89,7 +89,7 @@ export const dependencyVersionMap = { "@radix-ui/react-slot": "^1.2.3", // Icons (for shadcn/ui) - "lucide-react": "^0.577.0", + "lucide-react": "^1.16.0", // better-auth "better-auth": "^1.3.4", diff --git a/packages/cli/template/nextjs-shadcn/biome.json b/packages/cli/template/nextjs-shadcn/biome.json deleted file mode 100644 index 3ac108f5..00000000 --- a/packages/cli/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/template/nextjs-shadcn/src/app/(main)/page.tsx b/packages/cli/template/nextjs-shadcn/src/app/(main)/page.tsx index 6c9d5cd9..91016644 100644 --- a/packages/cli/template/nextjs-shadcn/src/app/(main)/page.tsx +++ b/packages/cli/template/nextjs-shadcn/src/app/(main)/page.tsx @@ -56,7 +56,6 @@ export default function Home() {
- {/** biome-ignore lint/performance/noImgElement: just a template image */} ProofKit; -} \ No newline at end of file +} 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 index a302ecce..d9b22d50 100644 --- a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/Header.tsx +++ b/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/Header.tsx @@ -1,7 +1,6 @@ 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"; 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 index ac2a2e2b..fdb4fe9d 100644 --- a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/HeaderMobileMenu.tsx +++ b/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/HeaderMobileMenu.tsx @@ -12,6 +12,7 @@ export default function HeaderMobileMenu() { className="inline-flex h-8 w-8 items-center justify-center rounded border border-zinc-300 dark:border-zinc-700" aria-label="Open menu" onClick={() => setOpened((v) => !v)} + type="button" > 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 index 8e106d85..049e25a9 100644 --- a/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/HeaderNavLink.tsx +++ b/packages/cli/template/nextjs-shadcn/src/components/AppShell/internal/HeaderNavLink.tsx @@ -1,7 +1,6 @@ "use client"; import { usePathname } from "next/navigation"; -import React from "react"; import { type ProofKitRoute } from "@/app/proofkit-route"; import classes from "./Header.module.css"; @@ -11,7 +10,7 @@ export default function HeaderNavLink(route: ProofKitRoute) { if (route.type === "function") { return ( - ); 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 index f63d0365..591aff46 100644 --- 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 @@ -20,18 +20,19 @@ export function SlotHeaderMobileMenuContent({ return (
{primaryRoutes.map((route) => ( - diff --git a/packages/cli/template/nextjs-shadcn/src/components/ui/button.tsx b/packages/cli/template/nextjs-shadcn/src/components/ui/button.tsx index 30f83b65..0a0b4142 100644 --- a/packages/cli/template/nextjs-shadcn/src/components/ui/button.tsx +++ b/packages/cli/template/nextjs-shadcn/src/components/ui/button.tsx @@ -1,5 +1,6 @@ import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; +import type { VariantProps } from "class-variance-authority"; import type * as React from "react"; import { cn } from "@/lib/utils"; @@ -7,35 +8,36 @@ 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", - 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, + extends + React.ButtonHTMLAttributes, VariantProps { asChild?: boolean; } @@ -51,7 +53,7 @@ function Button({ const Comp = asChild ? Slot : "button"; return ( diff --git a/packages/cli/template/nextjs-shadcn/src/lib/utils.ts b/packages/cli/template/nextjs-shadcn/src/lib/utils.ts index bd0c391d..73be6201 100644 --- a/packages/cli/template/nextjs-shadcn/src/lib/utils.ts +++ b/packages/cli/template/nextjs-shadcn/src/lib/utils.ts @@ -1,6 +1,7 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import { clsx } from "clsx"; +import type { ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } diff --git a/packages/cli/template/vite-wv/package.json b/packages/cli/template/vite-wv/package.json index ca23da65..859ba883 100644 --- a/packages/cli/template/vite-wv/package.json +++ b/packages/cli/template/vite-wv/package.json @@ -26,12 +26,12 @@ "@proofkit/typegen": "^1.1.0-beta.16", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.2.0", + "@vitejs/plugin-react": "^6.0.1", "dotenv": "^17.3.1", "open": "^11.0.0", - "typescript": "^5.9.3", + "typescript": "^6.0.3", "ultracite": "7.0.8", - "vite": "^7.3.1", + "vite": "^8.0.13", "vite-plugin-singlefile": "^2.3.2" } } diff --git a/packages/cli/template/vite-wv/scripts/filemaker.js b/packages/cli/template/vite-wv/scripts/filemaker.js index 1498ec2a..42942904 100644 --- a/packages/cli/template/vite-wv/scripts/filemaker.js +++ b/packages/cli/template/vite-wv/scripts/filemaker.js @@ -1,19 +1,18 @@ import { resolve } from "node:path"; + import dotenv from "dotenv"; -import { fileURLToPath } from "node:url"; -const currentDirectory = fileURLToPath(new URL(".", import.meta.url)); +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 defaultFmMcpBaseUrl = + process.env.FM_MCP_BASE_URL ?? "http://127.0.0.1:1365"; -function stripFileExtension(fileName) { - return fileName.replace(/\.fmp12$/i, ""); -} +const stripFileExtension = (fileName) => fileName.replace(/\.fmp12$/iu, ""); -async function getConnectedFiles(baseUrl = defaultFmMcpBaseUrl) { +const getConnectedFiles = async (baseUrl = defaultFmMcpBaseUrl) => { const healthResponse = await fetch(`${baseUrl}/health`).catch(() => null); if (!healthResponse?.ok) { return []; @@ -24,23 +23,26 @@ async function getConnectedFiles(baseUrl = defaultFmMcpBaseUrl) { .catch(() => []); return Array.isArray(connectedFiles) ? connectedFiles : []; -} +}; -async function isBridgeReachable(baseUrl = defaultFmMcpBaseUrl) { +const isBridgeReachable = async (baseUrl = defaultFmMcpBaseUrl) => { const healthResponse = await fetch(`${baseUrl}/health`).catch(() => null); return healthResponse?.ok === true; -} +}; -function normalizeTarget(fileName) { - return stripFileExtension(fileName).toLowerCase(); -} +const normalizeTarget = (fileName) => + stripFileExtension(fileName).toLowerCase(); -export async function resolveFileMakerTarget() { +export const resolveFileMakerTarget = async () => { const connectedFiles = await getConnectedFiles(); - const targetFromEnv = process.env.FM_DATABASE ? normalizeTarget(process.env.FM_DATABASE) : undefined; + const targetFromEnv = process.env.FM_DATABASE + ? normalizeTarget(process.env.FM_DATABASE) + : undefined; if (targetFromEnv) { - const matches = connectedFiles.filter((connectedFile) => normalizeTarget(connectedFile) === targetFromEnv); + const matches = connectedFiles.filter( + (connectedFile) => normalizeTarget(connectedFile) === targetFromEnv + ); if (matches.length === 1) { return { fileName: stripFileExtension(matches[0]), @@ -51,7 +53,7 @@ export async function resolveFileMakerTarget() { 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.`, + `FM_DATABASE is set to "${process.env.FM_DATABASE}" but no matching connected file was found via FM MCP.` ); } } @@ -66,7 +68,7 @@ export async function resolveFileMakerTarget() { if (connectedFiles.length > 1) { throw new Error( - `Multiple FileMaker files are connected via FM MCP (${connectedFiles.join(", ")}). Set FM_DATABASE to choose one.`, + `Multiple FileMaker files are connected via FM MCP (${connectedFiles.join(", ")}). Set FM_DATABASE to choose one.` ); } @@ -76,9 +78,9 @@ export async function resolveFileMakerTarget() { if (serverValue && databaseValue) { let hostname; try { - hostname = new URL(serverValue).hostname; + ({ hostname } = new URL(serverValue)); } catch { - hostname = serverValue.replace(/^https?:\/\//, "").replace(/\/.*$/, ""); + hostname = serverValue.replace(/^https?:\/\//u, "").replace(/\/.*$/u, ""); } return { @@ -89,22 +91,22 @@ export async function resolveFileMakerTarget() { } return null; -} +}; -export async function callFileMakerScript({ +export const callFileMakerScript = async ({ baseUrl = defaultFmMcpBaseUrl, connectedFileName, scriptName, data, -}) { +}) => { const response = await fetch(`${baseUrl}/callScript`, { - method: "POST", - headers: { "content-type": "application/json" }, body: JSON.stringify({ connectedFileName, - scriptName, 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, @@ -121,18 +123,22 @@ export async function callFileMakerScript({ throw new Error(errorMessage); } - if (!payload || typeof payload.fetchId !== "string" || !("result" in payload)) { + if ( + !payload || + typeof payload.fetchId !== "string" || + !("result" in payload) + ) { throw new Error("Invalid response from FM MCP bridge /callScript."); } return payload; -} +}; -export async function deployHtml({ +export const deployHtml = async ({ appName, path, scriptName = "deploy_html", -}) { +}) => { const target = await resolveFileMakerTarget(); if (!target) { return { @@ -142,14 +148,15 @@ export async function deployHtml({ } const payload = { appName, path }; - const bridgeAvailable = target.source === "fm-mcp" && (await isBridgeReachable()); + const bridgeAvailable = + target.source === "fm-mcp" && (await isBridgeReachable()); if (bridgeAvailable) { try { const bridgeResult = await callFileMakerScript({ connectedFileName: target.fileName, - scriptName, data: payload, + scriptName, }); return { method: "bridge", @@ -168,19 +175,19 @@ export async function deployHtml({ method: "url", target, url: buildFmpUrl({ - host: target.host, fileName: target.fileName, - scriptName, + host: target.host, parameter, + scriptName, }), }; -} +}; -export function buildFmpUrl({ host, fileName, scriptName, parameter }) { +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 index 4ba927fe..d39cef51 100644 --- a/packages/cli/template/vite-wv/scripts/upload.js +++ b/packages/cli/template/vite-wv/scripts/upload.js @@ -1,10 +1,11 @@ +import { resolve } from "node:path"; + import open from "open"; -import { resolve } from "path"; -import { fileURLToPath } from "url"; -import { deployHtml } from "./filemaker.js"; + import packageJson from "../package.json" with { type: "json" }; +import { deployHtml } from "./filemaker.js"; -const currentDirectory = fileURLToPath(new URL(".", import.meta.url)); +const currentDirectory = import.meta.dirname; const thePath = resolve(currentDirectory, "../dist", "index.html"); const deployment = await deployHtml({ appName: packageJson.name, @@ -13,7 +14,7 @@ const deployment = await deployHtml({ 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.", + "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); } diff --git a/packages/cli/template/vite-wv/src/App.tsx b/packages/cli/template/vite-wv/src/app.tsx similarity index 98% rename from packages/cli/template/vite-wv/src/App.tsx rename to packages/cli/template/vite-wv/src/app.tsx index 82be44fb..d29646c5 100644 --- a/packages/cli/template/vite-wv/src/App.tsx +++ b/packages/cli/template/vite-wv/src/app.tsx @@ -61,7 +61,7 @@ export default function App() {