Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changeset/clean-suns-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"@proofkit/typegen": patch
---

Make `@proofkit/fmdapi` and `@proofkit/fmodata` optional peers for `@proofkit/typegen`, and lazy-load each path so fmdapi-only and fmodata-only installs do not hard-require the other package.
5 changes: 5 additions & 0 deletions .changeset/cli-init-claude-cursorignore.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@proofkit/cli": patch
---

Init now writes `CLAUDE.md` as `@AGENTS.md` and adds `.cursorignore` to keep `CLAUDE.md` out of Cursor scans.
4 changes: 4 additions & 0 deletions .changeset/fair-lamps-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"@proofkit/typegen": patch
---

Widen OData client error typing to include message and details payloads from env/config validation.
5 changes: 5 additions & 0 deletions .changeset/fix-cli-dot-name-normalization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@proofkit/cli": patch
---

Normalize and validate `.`-derived CLI project names from the current directory consistently, including whitespace-to-dash conversion and lowercasing
5 changes: 5 additions & 0 deletions .changeset/fix-cli-parse-name-parent-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@proofkit/cli": patch
---

Normalize only the final path segment in `parseNameAndPath`, preserving leading directory segments verbatim while keeping scoped-name parsing and `.` handling intact
5 changes: 5 additions & 0 deletions .changeset/mean-worms-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@proofkit/cli": patch
---

Drop the unused `nextjs-mantine` scaffold from the current CLI and always scaffold browser apps from `nextjs-shadcn`.
5 changes: 5 additions & 0 deletions .changeset/remove-cli-ui-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@proofkit/cli": patch
---

Remove the `--ui` init flag. ProofKit now only scaffolds shadcn.
5 changes: 5 additions & 0 deletions .changeset/soften-project-name-spaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@proofkit/cli": patch
---

Allow spaces in project names by normalizing them to dashes
5 changes: 5 additions & 0 deletions .changeset/tidy-dots-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@proofkit/cli": patch
---

Clarify that `.` uses the current directory for `proofkit init`
2 changes: 1 addition & 1 deletion apps/docs/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "./global.css";
import type { Metadata } from "next";
import { RootProvider } from "fumadocs-ui/provider/next";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import type { ReactNode } from "react";

Expand Down
41 changes: 10 additions & 31 deletions packages/cli/src/cli/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@ interface CliFlags {
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. */
Expand Down Expand Up @@ -77,16 +75,13 @@ const defaultOptions: CliFlags = {
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)
Expand Down Expand Up @@ -198,8 +193,7 @@ export const runInit = async (name?: string, opts?: CliFlags) => {
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";
state.ui = "shadcn";

let projectName = name;
if (!projectName) {
Expand Down Expand Up @@ -299,30 +293,15 @@ export const runInit = async (name?: string, opts?: CliFlags) => {
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: [],
};
// 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
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/src/core/planInit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,12 @@ export function planInit(
path: path.join(targetDir, ".env"),
content: createEnvFileContent(),
},
writes: [],
writes: [
{
path: path.join(targetDir, ".cursorignore"),
content: "CLAUDE.md\n",
},
],
commands: [
...(request.noInstall ? [] : [{ type: "install" as const }]),
...(request.dataSource === "filemaker" &&
Expand Down
31 changes: 3 additions & 28 deletions packages/cli/src/helpers/createProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ export const createBareProject = async ({
devMode: true,
});

// Add new base dependencies for Tailwind v4 and shadcn/ui or legacy Mantine
// These should match the plan and dependencyVersionMap
// Add base deps for current templates. Legacy Mantine projects remain supported elsewhere.
const NEXT_SHADCN_BASE_DEPS = [
"@radix-ui/react-slot",
"@tailwindcss/postcss",
Expand Down Expand Up @@ -72,31 +71,7 @@ export const createBareProject = async ({
const SHADCN_BASE_DEV_DEPS = ["ultracite"] as AvailableDependencies[];
const VITE_SHADCN_BASE_DEV_DEPS = ["@proofkit/typegen", "ultracite"] 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",
"ultracite",
] as AvailableDependencies[];

if (state.ui === "mantine") {
addPackageDependency({
dependencies: MANTINE_DEPS,
devMode: false,
});
addPackageDependency({
dependencies: MANTINE_DEV_DEPS,
devMode: true,
});
} else if (state.ui === "shadcn") {
if (state.ui === "shadcn") {
addPackageDependency({
dependencies: state.appType === "webviewer" ? VITE_SHADCN_BASE_DEPS : NEXT_SHADCN_BASE_DEPS,
devMode: false,
Expand All @@ -106,7 +81,7 @@ export const createBareProject = async ({
devMode: true,
});
} else {
throw new Error(`Unsupported UI library: ${state.ui}`);
throw new Error(`Unsupported scaffold UI library: ${state.ui}`);
}

// Install the selected packages
Expand Down
8 changes: 2 additions & 6 deletions packages/cli/src/helpers/scaffoldProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,7 @@ export const scaffoldProject = async ({
}: 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",
);
const srcDir = path.join(PKG_ROOT, state.appType === "browser" ? "template/nextjs-shadcn" : "template/vite-wv");

if (noInstall) {
logger.info("");
Expand Down Expand Up @@ -129,6 +124,7 @@ export const scaffoldProject = async ({

// 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);

Expand Down
4 changes: 1 addition & 3 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const runDefaultCommand = (rawFlags?: Partial<CliFlags>) =>
});

const initDirectoryArg = optionalArg(textArg({ name: "dir" })).pipe(
withArgDescription("The project name or target directory"),
withArgDescription("The project name or target directory. Use `.` for the current directory, best when it is empty."),
);

function optionalTextOption(name: string, description: string) {
Expand Down Expand Up @@ -147,7 +147,6 @@ function makeInitCommand() {
{
dir: initDirectoryArg,
appType: optionalChoiceOption("app-type", ["browser", "webviewer"] as const, "The type of app to create"),
ui: optionalChoiceOption("ui", ["shadcn", "mantine"] as const, "The UI scaffold to create"),
server: optionalTextOption("server", "The URL of your FileMaker Server"),
adminApiKey: optionalTextOption("admin-api-key", "Admin API key for OttoFMS"),
fileName: optionalTextOption(
Expand Down Expand Up @@ -180,7 +179,6 @@ function makeInitCommand() {
const flags: CliFlags = {
...defaultCliFlags,
appType: getOrUndefined(options.appType),
ui: getOrUndefined(options.ui),
server: getOrUndefined(options.server),
adminApiKey: getOrUndefined(options.adminApiKey),
fileName: getOrUndefined(options.fileName),
Expand Down
5 changes: 1 addition & 4 deletions packages/cli/src/services/live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,10 @@ const fileSystemService = {
};

const templateService = {
getTemplateDir: (appType: AppType, ui: UIType) => {
getTemplateDir: (appType: AppType, _ui: UIType) => {
if (appType === "webviewer") {
return path.join(TEMPLATE_ROOT, "vite-wv");
}
if (ui === "mantine") {
return path.join(TEMPLATE_ROOT, "nextjs-mantine");
}
return path.join(TEMPLATE_ROOT, "nextjs-shadcn");
},
};
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const schema = z
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"),
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(),
Expand Down
18 changes: 11 additions & 7 deletions packages/cli/src/utils/parseNameAndPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import pathModule from "node:path";

import { removeTrailingSlash } from "./removeTrailingSlash.js";

const whitespaceRegex = /\s+/g;

/**
* Parses the appName and its path from the user input.
*
Expand All @@ -19,24 +21,26 @@ import { removeTrailingSlash } from "./removeTrailingSlash.js";
*/
export const parseNameAndPath = (rawInput: string) => {
const input = removeTrailingSlash(rawInput);

const paths = input.split("/");
const normalizedPaths = [...paths];
const lastPathIndex = normalizedPaths.length - 1;

let appName = paths.at(-1) ?? "";
let appName = (normalizedPaths.at(-1) ?? "").replace(whitespaceRegex, "-").toLowerCase();
normalizedPaths[lastPathIndex] = appName;
Comment on lines +25 to +29
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical Bug: Incorrect normalization order for scoped packages

The code normalizes only the last path segment before checking for scoped packages:

let appName = (normalizedPaths.at(-1) ?? "").replace(whitespaceRegex, "-").toLowerCase();
normalizedPaths[lastPathIndex] = appName;

But when a scoped package is detected, it joins from normalizedPaths which has only the last segment normalized:

appName = normalizedPaths.slice(indexOfDelimiter).join("/");

Example: Input "@My Scope/My App" produces paths = ["@My Scope", "My App"], then normalizedPaths = ["@My Scope", "my-app"], and finally appName = "@My Scope/my-app" (scope not normalized).

This conflicts with projectName.ts which normalizes ALL package segments. The scope segment "@My Scope" should be normalized to "@my-scope". Fix by normalizing the scope segment:

const indexOfDelimiter = normalizedPaths.findIndex((p) => p.startsWith("@"));
if (indexOfDelimiter !== -1) {
  // Normalize the scope segment as well
  normalizedPaths[indexOfDelimiter] = normalizedPaths[indexOfDelimiter].replace(whitespaceRegex, "-").toLowerCase();
  appName = normalizedPaths.slice(indexOfDelimiter).join("/");
}
Suggested change
const normalizedPaths = [...paths];
const lastPathIndex = normalizedPaths.length - 1;
let appName = paths.at(-1) ?? "";
let appName = (normalizedPaths.at(-1) ?? "").replace(whitespaceRegex, "-").toLowerCase();
normalizedPaths[lastPathIndex] = appName;
const normalizedPaths = [...paths];
const lastPathIndex = normalizedPaths.length - 1;
let appName = (normalizedPaths.at(-1) ?? "").replace(whitespaceRegex, "-").toLowerCase();
normalizedPaths[lastPathIndex] = appName;
const indexOfDelimiter = normalizedPaths.findIndex((p) => p.startsWith("@"));
if (indexOfDelimiter !== -1) {
// Normalize the scope segment as well
normalizedPaths[indexOfDelimiter] = normalizedPaths[indexOfDelimiter].replace(whitespaceRegex, "-").toLowerCase();
appName = normalizedPaths.slice(indexOfDelimiter).join("/");
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.


// 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);
appName = pathModule.basename(parsedCwd).replace(whitespaceRegex, "-").toLowerCase();
}

// 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 indexOfDelimiter = normalizedPaths.findIndex((p) => p.startsWith("@"));
if (indexOfDelimiter !== -1) {
appName = normalizedPaths.slice(indexOfDelimiter).join("/");
}

const path = paths.filter((p) => !p.startsWith("@")).join("/");
const path = normalizedPaths.filter((p) => !p.startsWith("@")).join("/");

return [appName, path] as const;
};
31 changes: 19 additions & 12 deletions packages/cli/src/utils/projectName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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) {
Expand All @@ -12,30 +13,36 @@ 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 normalized = trimTrailingSlashes(projectName);
const segments = normalized.split("/");
let scopedAppName = segments.at(-1) ?? "";
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 = path.basename(path.resolve(process.cwd()));
}

const scopeIndex = segments.findIndex((segment) => segment.startsWith("@"));
if (scopeIndex !== -1) {
scopedAppName = segments.slice(scopeIndex).join("/");
scopedAppName = normalizeProjectNameForPackage(path.basename(path.resolve(process.cwd())));
appDirPackageSegments = packageSegments;
}

const appDir = segments.filter((segment) => !segment.startsWith("@")).join("/");
const appDir = [...leadingSegments, ...appDirPackageSegments].join("/");

return [scopedAppName, appDir];
}

export function validateAppName(projectName: string) {
const normalized = trimTrailingSlashes(projectName);
const normalized = normalizeProjectNameForPackage(projectName);
if (normalized === ".") {
const currentDirName = path.basename(path.resolve(process.cwd()));
return VALID_APP_NAME_REGEX.test(currentDirName)
return VALID_APP_NAME_REGEX.test(currentDirName.replace(WHITESPACE_REGEX, "-").toLowerCase())
? undefined
: "Name must consist of only lowercase alphanumeric characters, '-', and '_'";
}
Expand Down
Loading
Loading