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/twenty-months-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@proofgeist/kit": minor
---

New "remove" command to remove pages, schemas, and data sources via the CLI
1 change: 0 additions & 1 deletion cli/src/cli/add/data-source/filemaker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,6 @@ export async function promptForFileMakerDataSource({
setSettings(settings);

addToFmschemaConfig({
projectDir,
dataSourceName: name,
project,
envNames: name === "filemaker" ? undefined : newDataSource.envNames,
Expand Down
2 changes: 1 addition & 1 deletion cli/src/cli/add/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const runAdd = async (name: string | undefined) => {
const settings = getSettings();

if (name === "tanstack-query") {
return await runAddTanstackQueryCommand({ settings });
return await runAddTanstackQueryCommand();
}

const addType = abortIfCancel(
Expand Down
1 change: 0 additions & 1 deletion cli/src/cli/add/page/post-install/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export const postInstallTable: TPostInstallFn = async ({
});

const allFieldNames = getFieldNamesForSchema({
projectDir,
schemaName,
dataSourceName: dataSource.name,
});
Expand Down
2 changes: 1 addition & 1 deletion cli/src/cli/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ export const runInit = async (name?: string, opts?: CliFlags) => {

if (!cliOptions.noInstall) {
await installDependencies({ projectDir });
await runCodegenCommand({ projectDir });
await runCodegenCommand();
}

if (!cliOptions.noGit) {
Expand Down
9 changes: 9 additions & 0 deletions cli/src/cli/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { DOCS_URL } from "~/consts.js";
import { getSettings } from "~/utils/parseSettings.js";
import { runAdd } from "./add/index.js";
import { runDeploy } from "./deploy/index.js";
import { runRemove } from "./remove/index.js";
import { runTypegen } from "./typegen/index.js";
import { runUpgrade } from "./update/index.js";
import { abortIfCancel } from "./utils.js";
Expand All @@ -22,6 +23,11 @@ export const runMenu = async () => {
value: "add",
hint: "Add new pages, schemas, data sources, etc.",
},
{
label: "Remove Components",
value: "remove",
hint: "Remove pages, schemas, data sources, etc.",
},
{
label: "Generate Types",
value: "typegen",
Expand Down Expand Up @@ -50,6 +56,9 @@ export const runMenu = async () => {
case "add":
await runAdd(undefined);
break;
case "remove":
await runRemove(undefined);
break;
case "docs":
p.log.info(`Opening ${chalk.cyan(DOCS_URL)} in your browser...`);
await open(DOCS_URL);
Expand Down
176 changes: 176 additions & 0 deletions cli/src/cli/remove/data-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import path from "path";
import * as p from "@clack/prompts";
import { Command } from "commander";
import dotenv from "dotenv";
import fs from "fs-extra";
import { z } from "zod";

import {
removeFromFmschemaConfig,
runCodegenCommand,
} from "~/generators/fmdapi.js";
import { ciOption, debugOption } from "~/globalOptions.js";
import { initProgramState, state } from "~/state.js";
import {
getSettings,
setSettings,
type DataSource,
} from "~/utils/parseSettings.js";
import { getNewProject } from "~/utils/ts-morph.js";
import {
abortIfCancel,
ensureProofKitProject,
UserAbortedError,
} from "../utils.js";

function getDataSourceInfo(source: DataSource) {
if (source.type !== "fm") {
return source.type;
}

const envFile = path.join(state.projectDir, ".env");
if (fs.existsSync(envFile)) {
dotenv.config({ path: envFile });
}

const server = process.env[source.envNames.server] || "unknown server";
const database = process.env[source.envNames.database] || "unknown database";

try {
// Format the server URL to be more readable
const serverUrl = new URL(server);
const formattedServer = serverUrl.hostname;
return `${formattedServer}/${database}`;
} catch (error) {
if (state.debug) {
console.error("Error parsing server URL:", error);
}
return `${server}/${database}`;
}
}

export const runRemoveDataSourceCommand = async (name?: string) => {
const settings = getSettings();

if (settings.dataSources.length === 0) {
p.note("No data sources found in your project.");
return;
}

let dataSourceName = name;

// If no name provided, prompt for selection
if (!dataSourceName) {
dataSourceName = abortIfCancel(
await p.select({
message: "Which data source do you want to remove?",
options: settings.dataSources.map((source) => {
let info = "";
try {
info = getDataSourceInfo(source);
} catch (error) {
if (state.debug) {
console.error("Error getting data source info:", error);
}
info = "unknown connection";
}
return {
label: `${source.name} (${info})`,
value: source.name,
};
}),
})
);
} else {
// Validate that the provided name exists
const dataSourceExists = settings.dataSources.some(
(source) => source.name === dataSourceName
);
if (!dataSourceExists) {
throw new Error(
`Data source "${dataSourceName}" not found in your project.`
);
}
}

let confirmed = true;
if (!state.ci) {
confirmed = abortIfCancel(
await p.confirm({
message: `Are you sure you want to remove the data source "${dataSourceName}"? This will only remove it from your configuration, not replace any possible usage, which may cause TypeScript errors.`,
})
);

if (!confirmed) throw new UserAbortedError();
}

// Get the data source before removing it
const dataSource = settings.dataSources.find(
(source) => source.name === dataSourceName
);

// Remove the data source from settings
settings.dataSources = settings.dataSources.filter(
(source) => source.name !== dataSourceName
);

// Save the updated settings
setSettings(settings);

if (dataSource?.type === "fm") {
// For FileMaker data sources, remove from fmschema.config.mjs
removeFromFmschemaConfig({
dataSourceName,
});

if (state.debug) {
p.note(`Removed schemas from fmschema.config.mjs`);
}

// Remove the schema folder for this data source
const schemaFolderPath = path.join(
state.projectDir,
"src",
"config",
"schemas",
dataSourceName
);
if (fs.existsSync(schemaFolderPath)) {
fs.removeSync(schemaFolderPath);
if (state.debug) {
p.note(`Removed schema folder at ${schemaFolderPath}`);
}
}

// Run typegen to regenerate types
await runCodegenCommand();
if (state.debug) {
p.note("Successfully regenerated types");
}
}

p.note(`Successfully removed data source "${dataSourceName}"`);
};

export const makeRemoveDataSourceCommand = () => {
const removeDataSourceCommand = new Command("data")
.description("Remove a data source from your project")
.option("--name <name>", "Name of the data source to remove")
.addOption(ciOption)
.addOption(debugOption)
.action(async (options) => {
const schema = z.object({
name: z.string().optional(),
});
const validated = schema.parse(options);
await runRemoveDataSourceCommand(validated.name);
});

removeDataSourceCommand.hook("preAction", (_thisCommand, actionCommand) => {
initProgramState(actionCommand.opts());
state.baseCommand = "remove";
ensureProofKitProject({ commandName: "remove" });
});

return removeDataSourceCommand;
};
75 changes: 75 additions & 0 deletions cli/src/cli/remove/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import * as p from "@clack/prompts";
import { Command } from "commander";

import { ciOption, debugOption } from "~/globalOptions.js";
import { initProgramState, state } from "~/state.js";
import { getSettings } from "~/utils/parseSettings.js";
import { abortIfCancel, ensureProofKitProject } from "../utils.js";
import {
makeRemoveDataSourceCommand,
runRemoveDataSourceCommand,
} from "./data-source.js";
import { makeRemovePageCommand, runRemovePageAction } from "./page.js";
import { makeRemoveSchemaCommand, runRemoveSchemaAction } from "./schema.js";

export const runRemove = async (name: string | undefined) => {
const settings = getSettings();

const removeType = abortIfCancel(
await p.select({
message: "What do you want to remove from your project?",
options: [
{ label: "Page", value: "page" },
{
label: "Schema",
value: "schema",
hint: "remove a table or layout schema",
},
...(settings.appType === "browser"
? [
{
label: "Data Source",
value: "data",
hint: "remove a database or FileMaker connection",
},
]
: []),
],
})
);

if (removeType === "data") {
await runRemoveDataSourceCommand();
} else if (removeType === "page") {
await runRemovePageAction();
} else if (removeType === "schema") {
await runRemoveSchemaAction();
}
};

export function makeRemoveCommand() {
const removeCommand = new Command("remove")
.description("Remove a component from your project")
.argument("[name]", "Type of component to remove")
.addOption(ciOption)
.addOption(debugOption)
.action(runRemove);

removeCommand.hook("preAction", (_thisCommand, _actionCommand) => {
initProgramState(_actionCommand.opts());
state.baseCommand = "remove";
ensureProofKitProject({ commandName: "remove" });
});
removeCommand.hook("preSubcommand", (_thisCommand, _subCommand) => {
initProgramState(_subCommand.opts());
state.baseCommand = "remove";
ensureProofKitProject({ commandName: "remove" });
});

// Add subcommands
removeCommand.addCommand(makeRemoveDataSourceCommand());
removeCommand.addCommand(makeRemovePageCommand());
removeCommand.addCommand(makeRemoveSchemaCommand());

return removeCommand;
}
Loading