From 5c2172ec20b0fab9f3861ab6b1bdb20b6cffdbb2 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Tue, 26 Aug 2025 06:46:54 -0500 Subject: [PATCH 1/3] fmdapi template --- packages/cli/src/cli/add/index.ts | 107 +++++++++++++++--- .../cli/src/cli/add/registry/getOptions.ts | 20 ++-- packages/cli/src/cli/add/registry/install.ts | 79 +++++++------ .../cli/src/cli/add/registry/listItems.ts | 9 ++ .../src/cli/add/registry/postInstall/index.ts | 5 + packages/cli/src/helpers/stealth-init.ts | 2 +- packages/cli/src/utils/addToEnvs.ts | 9 +- packages/cli/src/utils/parseSettings.ts | 17 ++- .../cli/template/nextjs-shadcn/proofkit.json | 2 +- packages/registry/lib/types.ts | 60 ++++++++-- packages/registry/lib/utils.ts | 16 ++- .../registry/templates/better-auth/auth.hbs | 2 +- packages/registry/templates/fmdapi/_meta.ts | 39 +++++++ .../fmdapi/proofkit-typegen.config.jsonc | 11 ++ 14 files changed, 290 insertions(+), 88 deletions(-) create mode 100644 packages/cli/src/cli/add/registry/listItems.ts create mode 100644 packages/registry/templates/fmdapi/_meta.ts create mode 100644 packages/registry/templates/fmdapi/proofkit-typegen.config.jsonc diff --git a/packages/cli/src/cli/add/index.ts b/packages/cli/src/cli/add/index.ts index 4d39ea0f..4ff75b09 100644 --- a/packages/cli/src/cli/add/index.ts +++ b/packages/cli/src/cli/add/index.ts @@ -1,13 +1,13 @@ import * as p from "@clack/prompts"; import { Command } from "commander"; +import { capitalize, groupBy, uniq } from "es-toolkit"; +import ora from "ora"; import { ciOption, debugOption } from "~/globalOptions.js"; import { initProgramState, state } from "~/state.js"; +import { logger } from "~/utils/logger.js"; import { getSettings } from "~/utils/parseSettings.js"; -import { - makeAddReactEmailCommand, - runAddReactEmailCommand, -} from "../react-email.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"; @@ -18,6 +18,85 @@ import { 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"; + +const runAddFromRegistry = async (options?: { noInstall?: boolean }) => { + const settings = getSettings(); + + const spinner = ora("Loading available components...").start(); + const items = await listItems(); + + 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 p.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" }] + : []), + { + label: "Schema", + value: "schema", + hint: "load data from a new table or layout from an existing data source", + }, + + ...(settings.appType === "browser" + ? [ + { + 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.includes(addType as any)) { + // one of the categories + const itemsFromCategory = + groupedByCategory[addType as keyof typeof groupedByCategory]; + + const itemName = abortIfCancel( + await p.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, @@ -31,9 +110,12 @@ export const runAdd = async ( } ensureProofKitProject({ commandName: "add" }); - const settings = getSettings(); + const settings = getSettings(); + if (settings.ui === "shadcn") { + return await runAddFromRegistry(options); + } const addType = abortIfCancel( await p.select({ @@ -55,22 +137,13 @@ export const runAdd = async ( }, ] : []), - ...(settings.ui === "shadcn" ? [] : settings.auth.type === "none" && settings.appType === "browser" + ...(settings.auth.type === "none" && settings.appType === "browser" ? [{ label: "Auth", value: "auth" }] : []), ], }) ); - // For shadcn projects, block adding new pages or auth for now - if (settings.ui === "shadcn") { - if (addType === "page" || addType === "auth") { - return p.cancel( - "Adding new pages or auth is not yet supported for shadcn-based projects." - ); - } - } - if (addType === "auth") { await runAddAuthAction(); } else if (addType === "data") { @@ -95,7 +168,9 @@ export const makeAddCommand = () => { "Do not run your package manager install command", false ) - .action(runAdd as any); + .action((name, options) => { + runAdd(name, options); + }); addCommand.hook("preAction", (_thisCommand, _actionCommand) => { // console.log("preAction", _actionCommand.opts()); diff --git a/packages/cli/src/cli/add/registry/getOptions.ts b/packages/cli/src/cli/add/registry/getOptions.ts index c932067b..f3fe8a9d 100644 --- a/packages/cli/src/cli/add/registry/getOptions.ts +++ b/packages/cli/src/cli/add/registry/getOptions.ts @@ -6,14 +6,20 @@ 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); + try { + 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; + } catch (error) { + throw error; } - return result.data; } const PROJECT_SHARED_IGNORE = [ diff --git a/packages/cli/src/cli/add/registry/install.ts b/packages/cli/src/cli/add/registry/install.ts index 9897196c..034a82f3 100644 --- a/packages/cli/src/cli/add/registry/install.ts +++ b/packages/cli/src/cli/add/registry/install.ts @@ -1,17 +1,21 @@ +import * as p from "@clack/prompts"; import { getOtherProofKitDependencies } from "@proofkit/registry"; -import { uniq, capitalize } from "es-toolkit"; +import { capitalize, uniq } from "es-toolkit"; import ora from "ora"; import semver from "semver"; -import * as p from "@clack/prompts"; -import { getRegistryUrl, shadcnInstall } from "~/helpers/shadcn-cli.js"; -import { getVersion } from "~/utils/getProofKitVersion.js"; -import { logger } from "~/utils/logger.js"; -import { getSettings, mergeSettings, type DataSource } from "~/utils/parseSettings.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 { abortIfCancel } from "~/cli/utils.js"; +import { getVersion } from "~/utils/getProofKitVersion.js"; +import { logger } from "~/utils/logger.js"; +import { + getSettings, + mergeSettings, + type DataSource, +} from "~/utils/parseSettings.js"; import { getMetaFromRegistry } from "./getOptions.js"; import { buildHandlebarsData, @@ -57,9 +61,9 @@ async function promptForSchemaFromDataSource({ export async function installFromRegistry(name: string) { const spinner = ora("Validating template").start(); - await preflightAddCommand(); try { + await preflightAddCommand(); const meta = await getMetaFromRegistry(name); if (!meta) { spinner.fail(`Template ${name} not found in the ProofKit registry`); @@ -87,31 +91,33 @@ export async function installFromRegistry(name: string) { 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."); + 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; + 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; @@ -149,11 +155,10 @@ export async function installFromRegistry(name: string) { pageName = capitalize(routeName.replace("/", "").trim()); } - let url = new URL(`${getRegistryUrl()}/r/${name}`); if (meta.category === "page") { - url.searchParams.set("routeName", `/(main)/${routeName??name}`); + url.searchParams.set("routeName", `/(main)/${routeName ?? name}`); } await shadcnInstall([url.toString()], meta.title); @@ -162,12 +167,13 @@ export async function installFromRegistry(name: string) { if (handlebarsFiles.length > 0) { // Build template data with schema information if available - const templateData = dataSource && schemaName - ? buildHandlebarsData({ - dataSource, - schemaName, - }) - : buildHandlebarsData(); + const templateData = + dataSource && schemaName + ? buildHandlebarsData({ + dataSource, + schemaName, + }) + : buildHandlebarsData(); // Add page information to template data if available if (routeName) { @@ -176,13 +182,16 @@ export async function installFromRegistry(name: string) { if (pageName) { (templateData as any).pageName = pageName; } - + // Resolve __PATH__ placeholders in file paths before handlebars processing - const resolvedFiles = handlebarsFiles.map(file => ({ + const resolvedFiles = handlebarsFiles.map((file) => ({ ...file, - destinationPath: file.destinationPath?.replace('__PATH__', `/(main)/${routeName??name}`) + destinationPath: file.destinationPath?.replace( + "__PATH__", + `/(main)/${routeName ?? name}` + ), })); - + for (const file of resolvedFiles) { await randerHandlebarsToFile(file, templateData); } diff --git a/packages/cli/src/cli/add/registry/listItems.ts b/packages/cli/src/cli/add/registry/listItems.ts new file mode 100644 index 00000000..046f5c73 --- /dev/null +++ b/packages/cli/src/cli/add/registry/listItems.ts @@ -0,0 +1,9 @@ +import { registryFetch } from "./http.js"; + +export async function listItems() { + const { data: items, error } = await registryFetch("@get/"); + if (error) { + throw new Error(`Failed to fetch items from registry: ${error.message}`); + } + return items; +} diff --git a/packages/cli/src/cli/add/registry/postInstall/index.ts b/packages/cli/src/cli/add/registry/postInstall/index.ts index bd2b8c0d..19a90fdc 100644 --- a/packages/cli/src/cli/add/registry/postInstall/index.ts +++ b/packages/cli/src/cli/add/registry/postInstall/index.ts @@ -1,5 +1,6 @@ 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"; @@ -11,6 +12,10 @@ export async function processPostInstallStep(step: PostInstallStep) { await wrapProvider(step); } else if (step.action === "next-steps") { logger.info(step.data.message); + } else if (step.action === "env") { + addToEnv({ + envs: step.data.envs, + }); } else { logger.error(`Unknown post-install step: ${step}`); } diff --git a/packages/cli/src/helpers/stealth-init.ts b/packages/cli/src/helpers/stealth-init.ts index 47a4a84a..ac7984f6 100644 --- a/packages/cli/src/helpers/stealth-init.ts +++ b/packages/cli/src/helpers/stealth-init.ts @@ -13,5 +13,5 @@ export async function stealthInit() { } // create proofkit.json - await fs.writeJson("proofkit.json", defaultSettings); + fs.writeJsonSync("proofkit.json", defaultSettings); } diff --git a/packages/cli/src/utils/addToEnvs.ts b/packages/cli/src/utils/addToEnvs.ts index 610dfda2..776ad1f7 100644 --- a/packages/cli/src/utils/addToEnvs.ts +++ b/packages/cli/src/utils/addToEnvs.ts @@ -2,8 +2,9 @@ import path from "path"; import fs from "fs-extra"; import { SyntaxKind, type Project } from "ts-morph"; -import { formatAndSaveSourceFiles, getNewProject } from "./ts-morph.js"; import { findT3EnvFile } from "~/installers/envVars.js"; +import { state } from "~/state.js"; +import { formatAndSaveSourceFiles, getNewProject } from "./ts-morph.js"; interface EnvSchema { name: string; @@ -15,17 +16,17 @@ interface EnvSchema { } export async function addToEnv({ - projectDir, + projectDir = state.projectDir, envs, envFileDescription, ...args }: { - projectDir: string; + projectDir?: string; project?: Project; envs: EnvSchema[]; envFileDescription?: string; }) { - const envSchemaFile = findT3EnvFile() + const envSchemaFile = findT3EnvFile(); const project = args.project ?? getNewProject(projectDir); const schemaFile = project.addSourceFileAtPath(envSchemaFile); diff --git a/packages/cli/src/utils/parseSettings.ts b/packages/cli/src/utils/parseSettings.ts index 155cf053..11f6c3e5 100644 --- a/packages/cli/src/utils/parseSettings.ts +++ b/packages/cli/src/utils/parseSettings.ts @@ -54,7 +54,7 @@ export type Ui = (typeof uiTypes)[number]; const settingsSchema = z.discriminatedUnion("ui", [ z.object({ ui: z.literal("mantine"), - appType: z.enum(appTypes).default("browser"), + appType: z.enum(appTypes).default("browser"), auth: authSchema, envFile: z.string().default(".env"), dataSources: z.array(dataSourceSchema).default([]), @@ -82,6 +82,11 @@ const settingsSchema = z.discriminatedUnion("ui", [ export const defaultSettings = settingsSchema.parse({ auth: { type: "none" }, ui: "shadcn", + appType: "browser", + envFile: ".env", + dataSources: [], + replacedMainPage: false, + registryTemplates: [], }); let settings: Settings | undefined; @@ -95,7 +100,15 @@ export const getSettings = () => { throw new Error(`ProofKit settings file not found at: ${settingsPath}`); } - const settingsFile: unknown = fs.readJSONSync(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); diff --git a/packages/cli/template/nextjs-shadcn/proofkit.json b/packages/cli/template/nextjs-shadcn/proofkit.json index 13d3916d..4e4ff5e3 100644 --- a/packages/cli/template/nextjs-shadcn/proofkit.json +++ b/packages/cli/template/nextjs-shadcn/proofkit.json @@ -2,5 +2,5 @@ "ui": "shadcn", "envFile": ".env", "appType": "browser", - "registryTemplates": ["utils/t3-env"] + "registryTemplates": ["utils/t3-env", "components/mode-toggle"] } diff --git a/packages/registry/lib/types.ts b/packages/registry/lib/types.ts index f70e7d84..f9b60ac5 100644 --- a/packages/registry/lib/types.ts +++ b/packages/registry/lib/types.ts @@ -41,9 +41,12 @@ export const templateFileSchema = z.discriminatedUnion("type", [ }), ]); -const buildPostInstallStepsSchema = ( - action: A, - dataSchema: T +const buildPostInstallStepsSchema = < + T extends z.AnyZodObject, + A extends string, +>( + action: A, + dataSchema: T, ) => { return z.object({ action: z.literal(action), @@ -53,15 +56,44 @@ const buildPostInstallStepsSchema = }; export const postInstallStepsSchema = z.discriminatedUnion("action", [ - buildPostInstallStepsSchema("next-steps" , z.object({ - message: z.string(), - })), - buildPostInstallStepsSchema("package.json script", z.object({ + buildPostInstallStepsSchema( + "next-steps", + z.object({ + message: z.string(), + }), + ), + buildPostInstallStepsSchema( + "package.json script", + z.object({ scriptName: z.string(), scriptCommand: z.string(), }), ), - buildPostInstallStepsSchema("wrap provider" , z.object({ + buildPostInstallStepsSchema( + "env", + z.object({ + envs: z + .object({ + name: z.string(), + zodValue: z.string(), + defaultValue: z + .string() + .optional() + .describe( + "This value will be added to the .env file, unless `addToRuntimeEnv` is set to `false`.", + ), + type: z.enum(["server", "client"]), + addToRuntimeEnv: z + .boolean() + .optional() + .describe("Whether to add the env to the runtime env."), + }) + .array(), + }), + ), + buildPostInstallStepsSchema( + "wrap provider", + z.object({ providerOpenTag: z .string() .describe( @@ -99,7 +131,7 @@ export const postInstallStepsSchema = z.discriminatedUnion("action", [ .describe( "If set, the provider will attempt to go inside of the parent tag. The first found tag will be used as the parent. If not set or none of the tags are found, the provider will be wrapped at the very top level.", ), - }), + }), ), ]); @@ -138,7 +170,9 @@ export const templateMetadataSchema = registryItemSchema schemaRequired: z .boolean() .optional() - .describe("Whether this template requires a database schema to be selected"), + .describe( + "Whether this template requires a database schema to be selected", + ), }); export type TemplateFile = z.infer; @@ -146,9 +180,11 @@ export type TemplateMetadata = z.infer; export const registryIndexSchema = templateMetadataSchema .pick({ title: true, category: true, description: true }) + .extend({ + name: z.string(), + }) .array(); - - +export type RegistryIndex = z.infer; export type RegistryItem = ShadcnRegistryItem; diff --git a/packages/registry/lib/utils.ts b/packages/registry/lib/utils.ts index 2c46b467..4f718b1d 100644 --- a/packages/registry/lib/utils.ts +++ b/packages/registry/lib/utils.ts @@ -4,6 +4,7 @@ import path from "path"; import { fileURLToPath } from "url"; import createJiti from "jiti"; import type { + RegistryIndex, RegistryItem, TemplateMetadata, } from "./types.js"; @@ -13,12 +14,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const defaultTemplatesPath = path.resolve(__dirname, "../templates"); -export type RegistryIndexItem = { - name: string; - type: TemplateMetadata["registryType"]; - category: TemplateMetadata["category"]; - // files: string[]; // destination paths -}; + /** * Scans the templates directory and returns all template directories with _meta.ts files @@ -73,16 +69,18 @@ function loadTemplateMeta(templatePath: string, templatesPath: string = defaultT }; } -export async function getRegistryIndex(templatesPath: string = defaultTemplatesPath): Promise { +export async function getRegistryIndex(templatesPath: string = defaultTemplatesPath): Promise { const templateDirs = getTemplateDirs(templatesPath); const index = templateDirs.map((templatePath) => { const meta = loadTemplateMeta(templatePath, templatesPath); - return { + const item: RegistryIndex[number] = { name: templatePath, // Use the path as the name - type: meta.registryType, category: meta.category, + title: meta.title, + description: meta.description, }; + return item; }); return index; diff --git a/packages/registry/templates/better-auth/auth.hbs b/packages/registry/templates/better-auth/auth.hbs index d7a7f4cb..4c722dff 100644 --- a/packages/registry/templates/better-auth/auth.hbs +++ b/packages/registry/templates/better-auth/auth.hbs @@ -8,7 +8,7 @@ import { GenericEmail } from "@/emails/generic"; export const auth = betterAuth({ // database database: FileMakerAdapter({ - debugLogs: true, + debugLogs: false, {{#findFirst proofkit.dataSources "fm"}} odata: { serverUrl: env.{{envNames.server}}, diff --git a/packages/registry/templates/fmdapi/_meta.ts b/packages/registry/templates/fmdapi/_meta.ts new file mode 100644 index 00000000..216e1eef --- /dev/null +++ b/packages/registry/templates/fmdapi/_meta.ts @@ -0,0 +1,39 @@ +import { TemplateMetadata } from "@proofkit/registry"; + +export const meta: TemplateMetadata = { + title: "@proofkit/fmdapi", + category: "utility", + registryType: "registry:lib", + dependencies: ["@proofkit/fmdapi"], + files: [ + { + sourceFileName: "proofkit-typegen.config.jsonc", + type: "registry:file", + destinationPath: "~/..", + }, + ], + postInstall: [ + { + action: "env", + data: { + envs: [ + { + name: "FM_SERVER", + type: "server", + zodValue: "z.string().startsWith('https://')", + }, + { + name: "FM_DATABASE", + type: "server", + zodValue: "z.string().endsWith('.fmp12')", + }, + { + name: "OTTO_API_KEY", + type: "server", + zodValue: "z.string().startsWith('dk_')", + }, + ], + }, + }, + ], +}; diff --git a/packages/registry/templates/fmdapi/proofkit-typegen.config.jsonc b/packages/registry/templates/fmdapi/proofkit-typegen.config.jsonc new file mode 100644 index 00000000..2ce5c9f8 --- /dev/null +++ b/packages/registry/templates/fmdapi/proofkit-typegen.config.jsonc @@ -0,0 +1,11 @@ +{ + "$schema": "https://proofkit.dev/typegen-config-schema.json", + "config": [ + { + "layouts": [], + "path": "./src/lib/schemas/filemaker", + "clearOldFiles": true, + "clientSuffix": "Layout", + }, + ], +} From dd7fb09ff1bd617fee951a012c9d381f3d8acb69 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Tue, 26 Aug 2025 11:05:04 -0500 Subject: [PATCH 2/3] Enhance error handling and fallback mechanisms in CLI. Update DEFAULT_REGISTRY_URL to provide a safe fallback when running from source. Improve settings management during stealth initialization and refine env file handling. Add preflight checks in the add command to ensure proper project setup. Update template metadata for fmdapi and t3-env. Refactor various utility functions for better clarity and error resilience. --- packages/cli/src/cli/add/index.ts | 27 +++++++--- packages/cli/src/cli/add/registry/install.ts | 16 +++++- packages/cli/src/consts.ts | 11 ++++- packages/cli/src/helpers/stealth-init.ts | 13 +++-- packages/cli/src/installers/envVars.ts | 23 +++++---- packages/cli/src/utils/addToEnvs.ts | 49 +++++++++++++++---- packages/cli/src/utils/formatting.ts | 1 + packages/cli/src/utils/parseSettings.ts | 28 +++++++++-- packages/registry/templates/fmdapi/_meta.ts | 3 +- .../registry/templates/utils/t3-env/_meta.ts | 3 +- .../registry/templates/utils/t3-env/env.ts | 2 +- 11 files changed, 139 insertions(+), 37 deletions(-) diff --git a/packages/cli/src/cli/add/index.ts b/packages/cli/src/cli/add/index.ts index 4ff75b09..e5e67c2e 100644 --- a/packages/cli/src/cli/add/index.ts +++ b/packages/cli/src/cli/add/index.ts @@ -6,7 +6,7 @@ import ora from "ora"; import { ciOption, debugOption } from "~/globalOptions.js"; import { initProgramState, state } from "~/state.js"; import { logger } from "~/utils/logger.js"; -import { getSettings } from "~/utils/parseSettings.js"; +import { getSettings, Settings } from "~/utils/parseSettings.js"; import { runAddReactEmailCommand } from "../react-email.js"; import { runAddTanstackQueryCommand } from "../tanstack-query.js"; import { abortIfCancel, ensureProofKitProject } from "../utils.js"; @@ -19,12 +19,20 @@ 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(); - const items = await listItems(); + let items; + 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) @@ -109,13 +117,18 @@ export const runAdd = async ( return await installFromRegistry(name); } - ensureProofKitProject({ commandName: "add" }); - - const settings = getSettings(); + 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 p.select({ @@ -168,8 +181,8 @@ export const makeAddCommand = () => { "Do not run your package manager install command", false ) - .action((name, options) => { - runAdd(name, options); + .action(async (name, options) => { + await runAdd(name, options); }); addCommand.hook("preAction", (_thisCommand, _actionCommand) => { diff --git a/packages/cli/src/cli/add/registry/install.ts b/packages/cli/src/cli/add/registry/install.ts index 034a82f3..e0701a90 100644 --- a/packages/cli/src/cli/add/registry/install.ts +++ b/packages/cli/src/cli/add/registry/install.ts @@ -83,7 +83,7 @@ export async function installFromRegistry(name: string) { spinner.succeed(); const otherProofKitDependencies = getOtherProofKitDependencies(meta); - const previouslyInstalledTemplates = getSettings().registryTemplates; + let previouslyInstalledTemplates = getSettings().registryTemplates; // Handle schema requirement if template needs it let dataSource: DataSource | undefined; @@ -161,6 +161,20 @@ export async function installFromRegistry(name: string) { 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); diff --git a/packages/cli/src/consts.ts b/packages/cli/src/consts.ts index 6c3bf932..48fb1f85 100644 --- a/packages/cli/src/consts.ts +++ b/packages/cli/src/consts.ts @@ -29,4 +29,13 @@ export const CREATE_FM_APP = cliName; // Registry URL is injected at build time via tsdown define declare const __REGISTRY_URL__: string; -export const DEFAULT_REGISTRY_URL = __REGISTRY_URL__; +// Provide a safe fallback when running from source (not built) +export const DEFAULT_REGISTRY_URL = + // typeof check avoids ReferenceError if not defined at runtime + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - __REGISTRY_URL__ is injected at build time + typeof __REGISTRY_URL__ !== "undefined" && __REGISTRY_URL__ + ? // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - __REGISTRY_URL__ is injected at build time + __REGISTRY_URL__ + : "https://proofkit.dev"; diff --git a/packages/cli/src/helpers/stealth-init.ts b/packages/cli/src/helpers/stealth-init.ts index ac7984f6..ac78c6be 100644 --- a/packages/cli/src/helpers/stealth-init.ts +++ b/packages/cli/src/helpers/stealth-init.ts @@ -1,6 +1,10 @@ import fs from "fs-extra"; -import { defaultSettings } from "~/utils/parseSettings.js"; +import { + defaultSettings, + setSettings, + validateAndSetEnvFile, +} from "~/utils/parseSettings.js"; /** * Used to add a proofkit.json file to an existing project @@ -12,6 +16,9 @@ export async function stealthInit() { return; } - // create proofkit.json - fs.writeJsonSync("proofkit.json", defaultSettings); + // create proofkit.json with default settings + setSettings(defaultSettings); + + // validate and set envFile only if it exists + validateAndSetEnvFile(); } diff --git a/packages/cli/src/installers/envVars.ts b/packages/cli/src/installers/envVars.ts index f75600bf..d397bec5 100644 --- a/packages/cli/src/installers/envVars.ts +++ b/packages/cli/src/installers/envVars.ts @@ -10,7 +10,7 @@ export type FMAuthKeys = | { ottoApiKey: string }; export const initEnvFile: Installer = () => { - const envFilePath = findT3EnvFile(false) ?? `./src/config/env.ts`; + const envFilePath = findT3EnvFile(false) ?? `./src/config/env.ts`; const envContent = ` # When adding additional environment variables, the schema in "${envFilePath}" @@ -25,27 +25,32 @@ export const initEnvFile: Installer = () => { fs.writeFileSync(envDest, envContent, "utf-8"); }; -export function findT3EnvFile(): string -export function findT3EnvFile(throwIfNotFound: false): string | null -export function findT3EnvFile(throwIfNotFound: true): string +export function findT3EnvFile(): string; +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 the T3 env files. Run "proofkit add utils/t3-env" to initilziate it`) - throw new Error("T3 env file not found") -} \ No newline at end of file + logger.warn( + `Could not find the T3 env files. Run "proofkit add utils/t3-env" to initilziate it` + ); + throw new Error("T3 env file not found"); +} diff --git a/packages/cli/src/utils/addToEnvs.ts b/packages/cli/src/utils/addToEnvs.ts index 776ad1f7..09605acf 100644 --- a/packages/cli/src/utils/addToEnvs.ts +++ b/packages/cli/src/utils/addToEnvs.ts @@ -1,3 +1,4 @@ +import { execSync } from "child_process"; import path from "path"; import fs from "fs-extra"; import { SyntaxKind, type Project } from "ts-morph"; @@ -38,21 +39,30 @@ export async function addToEnv({ .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]; + const opts = createEnvCall.getArguments()[0]; + if (!opts) { + throw new Error("createEnv call is missing options argument"); + } const serverProperty = opts - ?.getDescendantsOfKind(SyntaxKind.PropertyAssignment) + .getDescendantsOfKind(SyntaxKind.PropertyAssignment) .find((prop) => prop.getName() === "server") ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression); const clientProperty = opts - ?.getDescendantsOfKind(SyntaxKind.PropertyAssignment) + .getDescendantsOfKind(SyntaxKind.PropertyAssignment) .find((prop) => prop.getName() === "client") ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression); const runtimeEnvProperty = opts - ?.getDescendantsOfKind(SyntaxKind.PropertyAssignment) + .getDescendantsOfKind(SyntaxKind.PropertyAssignment) .find((prop) => prop.getName() === "experimental__runtimeEnv") ?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression); @@ -84,13 +94,32 @@ export async function addToEnv({ .join("\n"); const dotEnvFile = path.join(projectDir, ".env"); - const currentFile = fs.readFileSync(dotEnvFile, "utf-8"); - fs.writeFileSync( - dotEnvFile, - `${currentFile} + + // 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); diff --git a/packages/cli/src/utils/formatting.ts b/packages/cli/src/utils/formatting.ts index ac50477a..25959dbb 100644 --- a/packages/cli/src/utils/formatting.ts +++ b/packages/cli/src/utils/formatting.ts @@ -8,6 +8,7 @@ import { state } from "~/state.js"; * @param project The ts-morph Project containing the files to format */ export async function formatAndSaveSourceFiles(project: Project) { + project.saveSync(); // save here in case formatting fails try { const files = project.getSourceFiles(); // run each file through the prettier formatter diff --git a/packages/cli/src/utils/parseSettings.ts b/packages/cli/src/utils/parseSettings.ts index 11f6c3e5..9b238661 100644 --- a/packages/cli/src/utils/parseSettings.ts +++ b/packages/cli/src/utils/parseSettings.ts @@ -56,7 +56,7 @@ const settingsSchema = z.discriminatedUnion("ui", [ ui: z.literal("mantine"), appType: z.enum(appTypes).default("browser"), auth: authSchema, - envFile: z.string().default(".env"), + envFile: z.string().optional(), dataSources: z.array(dataSourceSchema).default([]), tanstackQuery: z.boolean().catch(false), replacedMainPage: z.boolean().catch(false), @@ -71,7 +71,7 @@ const settingsSchema = z.discriminatedUnion("ui", [ z.object({ ui: z.literal("shadcn"), appType: z.enum(appTypes).default("browser"), - envFile: z.string().default(".env"), + envFile: z.string().optional(), dataSources: z.array(dataSourceSchema).default([]), replacedMainPage: z.boolean().catch(false), registryUrl: z.url().optional(), @@ -83,7 +83,6 @@ export const defaultSettings = settingsSchema.parse({ auth: { type: "none" }, ui: "shadcn", appType: "browser", - envFile: ".env", dataSources: [], replacedMainPage: false, registryTemplates: [], @@ -132,3 +131,26 @@ export function setSettings(_settings: Settings) { 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/registry/templates/fmdapi/_meta.ts b/packages/registry/templates/fmdapi/_meta.ts index 216e1eef..43e65878 100644 --- a/packages/registry/templates/fmdapi/_meta.ts +++ b/packages/registry/templates/fmdapi/_meta.ts @@ -5,11 +5,12 @@ export const meta: TemplateMetadata = { category: "utility", registryType: "registry:lib", dependencies: ["@proofkit/fmdapi"], + // registryDependencies: ["{proofkit}/r/utils/t3-env"], files: [ { sourceFileName: "proofkit-typegen.config.jsonc", type: "registry:file", - destinationPath: "~/..", + destinationPath: "~/proofkit-typegen.config.jsonc", }, ], postInstall: [ diff --git a/packages/registry/templates/utils/t3-env/_meta.ts b/packages/registry/templates/utils/t3-env/_meta.ts index d29f0f70..94febd14 100644 --- a/packages/registry/templates/utils/t3-env/_meta.ts +++ b/packages/registry/templates/utils/t3-env/_meta.ts @@ -16,7 +16,8 @@ export const meta: TemplateMetadata = { { action: "next-steps", data: { - message: "Be sure to import the env.ts file into your next.config.ts to validate at build time.", + message: + "Be sure to import the env.ts file into your next.config.ts to validate at build time.", }, }, ], diff --git a/packages/registry/templates/utils/t3-env/env.ts b/packages/registry/templates/utils/t3-env/env.ts index 2a9149ea..9908f0aa 100644 --- a/packages/registry/templates/utils/t3-env/env.ts +++ b/packages/registry/templates/utils/t3-env/env.ts @@ -3,7 +3,7 @@ import { z } from "zod"; export const env = createEnv({ server: { - NODE_ENV: z.enum(["development", "production"]), + NODE_ENV: z.enum(["development", "production"]).catch("development"), }, client: {}, experimental__runtimeEnv: {}, From c71b0d4821904fe32b4c6a2952d0cac37f4121d3 Mon Sep 17 00:00:00 2001 From: Eric Luce <37158449+eluce2@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:23:01 -0500 Subject: [PATCH 3/3] Refactor initial settings structure in CLI initialization for improved readability. Update environment variable handling in the nextjs-shadcn template to use .catch for default values. --- .changeset/brown-keys-float.md | 5 +++ .../src/cli/add/registry/postInstall/index.ts | 2 +- packages/cli/src/cli/init.ts | 43 ++++++++++--------- packages/cli/src/installers/envVars.ts | 14 +++--- .../cli/template/nextjs-shadcn/src/lib/env.ts | 2 +- .../registry/templates/utils/t3-env/env.ts | 2 +- 6 files changed, 38 insertions(+), 30 deletions(-) create mode 100644 .changeset/brown-keys-float.md diff --git a/.changeset/brown-keys-float.md b/.changeset/brown-keys-float.md new file mode 100644 index 00000000..431dc0d5 --- /dev/null +++ b/.changeset/brown-keys-float.md @@ -0,0 +1,5 @@ +--- +"@proofkit/cli": patch +--- + +Add utils/fmdapi to registry diff --git a/packages/cli/src/cli/add/registry/postInstall/index.ts b/packages/cli/src/cli/add/registry/postInstall/index.ts index 19a90fdc..6afb4233 100644 --- a/packages/cli/src/cli/add/registry/postInstall/index.ts +++ b/packages/cli/src/cli/add/registry/postInstall/index.ts @@ -13,7 +13,7 @@ export async function processPostInstallStep(step: PostInstallStep) { } else if (step.action === "next-steps") { logger.info(step.data.message); } else if (step.action === "env") { - addToEnv({ + await addToEnv({ envs: step.data.envs, }); } else { diff --git a/packages/cli/src/cli/init.ts b/packages/cli/src/cli/init.ts index 9906d1e5..397d973e 100644 --- a/packages/cli/src/cli/init.ts +++ b/packages/cli/src/cli/init.ts @@ -246,26 +246,29 @@ export const runInit = async (name?: string, opts?: CliFlags) => { }); // 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: ["cursorRules"], - reactEmail: false, - reactEmailServer: false, - registryTemplates: [], - }:{ - appType: state.appType ?? "browser", - ui: "shadcn", - envFile: ".env", - dataSources: [], - replacedMainPage: false, - registryTemplates: [], - } + const initialSettings: Settings = + state.ui === "mantine" + ? { + appType: state.appType ?? "browser", + ui: "mantine", + auth: { type: "none" }, + envFile: ".env", + dataSources: [], + tanstackQuery: false, + replacedMainPage: false, + appliedUpgrades: ["cursorRules"], + reactEmail: false, + reactEmailServer: false, + registryTemplates: [], + } + : { + appType: state.appType ?? "browser", + ui: "shadcn", + envFile: ".env", + dataSources: [], + replacedMainPage: false, + registryTemplates: [], + }; const { registryUrl } = setSettings(initialSettings); // for webviewer apps FM is required, so don't ask diff --git a/packages/cli/src/installers/envVars.ts b/packages/cli/src/installers/envVars.ts index d397bec5..7472736a 100644 --- a/packages/cli/src/installers/envVars.ts +++ b/packages/cli/src/installers/envVars.ts @@ -30,12 +30,12 @@ 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`, + `src/config/env.ts`, + `src/lib/env.ts`, + `src/env.ts`, + `lib/env.ts`, + `env.ts`, + `config/env.ts`, ]; for (const testPath of possiblePaths) { @@ -50,7 +50,7 @@ export function findT3EnvFile(throwIfNotFound?: boolean): string | null { } logger.warn( - `Could not find the T3 env files. Run "proofkit add utils/t3-env" to initilziate it` + `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/template/nextjs-shadcn/src/lib/env.ts b/packages/cli/template/nextjs-shadcn/src/lib/env.ts index 2139d54c..83518a22 100644 --- a/packages/cli/template/nextjs-shadcn/src/lib/env.ts +++ b/packages/cli/template/nextjs-shadcn/src/lib/env.ts @@ -5,7 +5,7 @@ export const env = createEnv({ server: { NODE_ENV: z .enum(["development", "test", "production"]) - .default("development"), + .catch("development"), }, client: {}, experimental__runtimeEnv: {}, diff --git a/packages/registry/templates/utils/t3-env/env.ts b/packages/registry/templates/utils/t3-env/env.ts index 9908f0aa..d25ff6ab 100644 --- a/packages/registry/templates/utils/t3-env/env.ts +++ b/packages/registry/templates/utils/t3-env/env.ts @@ -3,7 +3,7 @@ import { z } from "zod"; export const env = createEnv({ server: { - NODE_ENV: z.enum(["development", "production"]).catch("development"), + NODE_ENV: z.enum(["development", "production"]).default("development"), }, client: {}, experimental__runtimeEnv: {},