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
5 changes: 5 additions & 0 deletions .changeset/brown-keys-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@proofkit/cli": patch
---

Add utils/fmdapi to registry
Comment on lines +1 to +5
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Ensure all impacted packages have a changeset entry

This PR also modifies registry code; only @proofkit/cli is bumped here. Verify @proofkit/registry (and any other changed packages) have corresponding changesets so they get published.

Run to confirm:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Packages referenced in changesets:"
rg -nP '^\s*"[.@/\w-]+"\s*:\s*(patch|minor|major)' .changeset/*.md || true

echo -e "\nCheck for @proofkit/registry entry:"
rg -n '@proofkit/registry' .changeset/*.md || echo "Missing @proofkit/registry changeset?"

Length of output: 873


Add a changeset entry for @proofkit/registry – registry code was updated but no corresponding .changeset bump exists; add one to ensure it’s published.

🧰 Tools
🪛 LanguageTool

[grammar] ~5-~5: There might be a mistake here.
Context: ...ofkit/cli": patch --- Add utils/fmdapi to registry

(QB_NEW_EN)

🤖 Prompt for AI Agents
In .changeset/brown-keys-float.md around lines 1 to 5, the changeset only lists
"@proofkit/cli" but the registry package was changed; add an entry for
"@proofkit/registry" (likely a patch bump) to this changeset so the registry
changes are published. Update the YAML frontmatter to include
"@proofkit/registry": patch (or the appropriate release type) and add a short
description line referencing the registry changes, then save the changeset so
the release tooling picks it up.

126 changes: 107 additions & 19 deletions packages/cli/src/cli/add/index.ts
Original file line number Diff line number Diff line change
@@ -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 { getSettings } from "~/utils/parseSettings.js";
import {
makeAddReactEmailCommand,
runAddReactEmailCommand,
} from "../react-email.js";
import { logger } from "~/utils/logger.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";
import { makeAddAuthCommand, runAddAuthAction } from "./auth.js";
Expand All @@ -18,6 +18,93 @@ 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";
import { preflightAddCommand } from "./registry/preflight.js";

const runAddFromRegistry = async (options?: { noInstall?: boolean }) => {
const settings = getSettings();

const spinner = ora("Loading available components...").start();
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)
);

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,
Expand All @@ -30,10 +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({
Expand All @@ -55,22 +150,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") {
Expand All @@ -95,7 +181,9 @@ export const makeAddCommand = () => {
"Do not run your package manager install command",
false
)
.action(runAdd as any);
.action(async (name, options) => {
await runAdd(name, options);
});

addCommand.hook("preAction", (_thisCommand, _actionCommand) => {
// console.log("preAction", _actionCommand.opts());
Expand Down
20 changes: 13 additions & 7 deletions packages/cli/src/cli/add/registry/getOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
95 changes: 59 additions & 36 deletions packages/cli/src/cli/add/registry/install.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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`);
Expand All @@ -79,39 +83,41 @@ 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;
let schemaName: string | undefined;
let routeName: string | undefined;
let pageName: string | undefined;


if (meta.schemaRequired) {
const settings = getSettings();

if (settings.dataSources.length === 0) {
spinner.fail("This template requires a data source, but you don't have any. Add a data source first.");
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;
Expand Down Expand Up @@ -149,25 +155,39 @@ 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}`);
}

// a (hopefully) temporary workaround because the shadcn command installs the env file in the wrong place if it's a dependency
if (
name === "fmdapi" &&
!previouslyInstalledTemplates.includes("utils/t3-env") &&
// this last guard will allow this workaroudn to be bypassed if the registry server updates to start serving the dependency again
meta.registryDependencies?.find((d) => d.includes("utils/t3-env")) ===
undefined
) {
// install the t3-env template manually first
await installFromRegistry("utils/t3-env");
previouslyInstalledTemplates = getSettings().registryTemplates;
}

// now install the template using shadcn-install
await shadcnInstall([url.toString()], meta.title);

const handlebarsFiles = meta.files.filter((file) => file.handlebars);

if (handlebarsFiles.length > 0) {
// Build template data with schema information if available
const 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) {
Expand All @@ -176,13 +196,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);
}
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/cli/add/registry/listItems.ts
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 5 additions & 0 deletions packages/cli/src/cli/add/registry/postInstall/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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") {
await addToEnv({
envs: step.data.envs,
});
} else {
logger.error(`Unknown post-install step: ${step}`);
}
Expand Down
Loading
Loading