mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
refractor: organize files
This commit is contained in:
5
.changeset/sharp-days-lead.md
Normal file
5
.changeset/sharp-days-lead.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"create-better-t-stack": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
refractor files
|
||||||
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -36,6 +36,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
publish: bun run publish-packages
|
publish: bun run publish-packages
|
||||||
env:
|
env:
|
||||||
MODE: "prod"
|
TELEMETRY: "true"
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
|
||||||
|
POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { execa } from "execa";
|
|||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { PackageManager, ProjectConfig } from "../../types";
|
import type { PackageManager, ProjectConfig } from "../../types";
|
||||||
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||||
import {
|
import {
|
||||||
addEnvVariablesToFile,
|
addEnvVariablesToFile,
|
||||||
type EnvVariable,
|
type EnvVariable,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import fs from "fs-extra";
|
|||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { PackageManager } from "../../types";
|
import type { PackageManager } from "../../types";
|
||||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||||
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||||
import {
|
import {
|
||||||
addEnvVariablesToFile,
|
addEnvVariablesToFile,
|
||||||
type EnvVariable,
|
type EnvVariable,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { type ExecaError, execa } from "execa";
|
|||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { PackageManager, ProjectConfig } from "../../types";
|
import type { PackageManager, ProjectConfig } from "../../types";
|
||||||
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||||
import {
|
import {
|
||||||
addEnvVariablesToFile,
|
addEnvVariablesToFile,
|
||||||
type EnvVariable,
|
type EnvVariable,
|
||||||
|
|||||||
181
apps/cli/src/helpers/project-generation/command-handlers.ts
Normal file
181
apps/cli/src/helpers/project-generation/command-handlers.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { cancel, intro, log, outro } from "@clack/prompts";
|
||||||
|
import fs from "fs-extra";
|
||||||
|
import pc from "picocolors";
|
||||||
|
import { DEFAULT_CONFIG } from "../../constants";
|
||||||
|
import { getAddonsToAdd } from "../../prompts/addons";
|
||||||
|
import { gatherConfig } from "../../prompts/config-prompts";
|
||||||
|
import { getProjectName } from "../../prompts/project-name";
|
||||||
|
import type { AddInput, CreateInput, ProjectConfig } from "../../types";
|
||||||
|
import { trackProjectCreation } from "../../utils/analytics";
|
||||||
|
import { displayConfig } from "../../utils/display-config";
|
||||||
|
import { generateReproducibleCommand } from "../../utils/generate-reproducible-command";
|
||||||
|
import {
|
||||||
|
handleDirectoryConflict,
|
||||||
|
setupProjectDirectory,
|
||||||
|
} from "../../utils/project-directory";
|
||||||
|
import { renderTitle } from "../../utils/render-title";
|
||||||
|
import { getProvidedFlags, processAndValidateFlags } from "../../validation";
|
||||||
|
import { addAddonsToProject } from "./add-addons";
|
||||||
|
import { createProject } from "./create-project";
|
||||||
|
import { detectProjectConfig } from "./detect-project-config";
|
||||||
|
|
||||||
|
export async function createProjectHandler(
|
||||||
|
input: CreateInput & { projectName?: string },
|
||||||
|
) {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
renderTitle();
|
||||||
|
intro(pc.magenta("Creating a new Better-T Stack project"));
|
||||||
|
|
||||||
|
let currentPathInput: string;
|
||||||
|
if (input.yes && input.projectName) {
|
||||||
|
currentPathInput = input.projectName;
|
||||||
|
} else if (input.yes) {
|
||||||
|
let defaultName = DEFAULT_CONFIG.relativePath;
|
||||||
|
let counter = 1;
|
||||||
|
while (
|
||||||
|
fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) &&
|
||||||
|
fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0
|
||||||
|
) {
|
||||||
|
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
currentPathInput = defaultName;
|
||||||
|
} else {
|
||||||
|
currentPathInput = await getProjectName(input.projectName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { finalPathInput, shouldClearDirectory } =
|
||||||
|
await handleDirectoryConflict(currentPathInput);
|
||||||
|
|
||||||
|
const { finalResolvedPath, finalBaseName } = await setupProjectDirectory(
|
||||||
|
finalPathInput,
|
||||||
|
shouldClearDirectory,
|
||||||
|
);
|
||||||
|
|
||||||
|
const cliInput = {
|
||||||
|
...input,
|
||||||
|
projectDirectory: input.projectName,
|
||||||
|
};
|
||||||
|
|
||||||
|
const providedFlags = getProvidedFlags(cliInput);
|
||||||
|
const flagConfig = processAndValidateFlags(
|
||||||
|
cliInput,
|
||||||
|
providedFlags,
|
||||||
|
finalBaseName,
|
||||||
|
);
|
||||||
|
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
|
||||||
|
|
||||||
|
if (!input.yes && Object.keys(otherFlags).length > 0) {
|
||||||
|
log.info(pc.yellow("Using these pre-selected options:"));
|
||||||
|
log.message(displayConfig(otherFlags));
|
||||||
|
log.message("");
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: ProjectConfig;
|
||||||
|
if (input.yes) {
|
||||||
|
config = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
...flagConfig,
|
||||||
|
projectName: finalBaseName,
|
||||||
|
projectDir: finalResolvedPath,
|
||||||
|
relativePath: finalPathInput,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.backend === "convex") {
|
||||||
|
log.info(
|
||||||
|
"Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo",
|
||||||
|
);
|
||||||
|
} else if (config.backend === "none") {
|
||||||
|
log.info(
|
||||||
|
"Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
pc.yellow("Using default/flag options (config prompts skipped):"),
|
||||||
|
);
|
||||||
|
log.message(displayConfig(config));
|
||||||
|
log.message("");
|
||||||
|
} else {
|
||||||
|
config = await gatherConfig(
|
||||||
|
flagConfig,
|
||||||
|
finalBaseName,
|
||||||
|
finalResolvedPath,
|
||||||
|
finalPathInput,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await createProject(config);
|
||||||
|
|
||||||
|
const reproducibleCommand = generateReproducibleCommand(config);
|
||||||
|
log.success(
|
||||||
|
pc.blue(
|
||||||
|
`You can reproduce this setup with the following command:\n${reproducibleCommand}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await trackProjectCreation(config);
|
||||||
|
|
||||||
|
const elapsedTimeInSeconds = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||||
|
outro(
|
||||||
|
pc.magenta(
|
||||||
|
`Project created successfully in ${pc.bold(
|
||||||
|
elapsedTimeInSeconds,
|
||||||
|
)} seconds!`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addAddonsHandler(input: AddInput): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!input.addons || input.addons.length === 0) {
|
||||||
|
const projectDir = input.projectDir || process.cwd();
|
||||||
|
const detectedConfig = await detectProjectConfig(projectDir);
|
||||||
|
|
||||||
|
if (!detectedConfig) {
|
||||||
|
cancel(
|
||||||
|
pc.red(
|
||||||
|
"Could not detect project configuration. Please ensure this is a valid Better-T Stack project.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const addonsPrompt = await getAddonsToAdd(
|
||||||
|
detectedConfig.frontend || [],
|
||||||
|
detectedConfig.addons || [],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (addonsPrompt.length === 0) {
|
||||||
|
outro(
|
||||||
|
pc.yellow(
|
||||||
|
"No addons to add or all compatible addons are already present.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addons = addonsPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input.addons || input.addons.length === 0) {
|
||||||
|
outro(pc.yellow("No addons specified to add."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await addAddonsToProject({
|
||||||
|
...input,
|
||||||
|
addons: input.addons,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import type {
|
|||||||
ProjectConfig,
|
ProjectConfig,
|
||||||
Runtime,
|
Runtime,
|
||||||
} from "../../types";
|
} from "../../types";
|
||||||
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||||
|
|
||||||
export function displayPostInstallInstructions(
|
export function displayPostInstallInstructions(
|
||||||
config: ProjectConfig & { depsInstalled: boolean },
|
config: ProjectConfig & { depsInstalled: boolean },
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import consola from "consola";
|
|||||||
import { execa } from "execa";
|
import { execa } from "execa";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { ProjectConfig } from "../../types";
|
import type { ProjectConfig } from "../../types";
|
||||||
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||||
|
|
||||||
export async function setupStarlight(config: ProjectConfig): Promise<void> {
|
export async function setupStarlight(config: ProjectConfig): Promise<void> {
|
||||||
const { packageManager, projectDir } = config;
|
const { packageManager, projectDir } = config;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import fs from "fs-extra";
|
|||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { ProjectConfig } from "../../types";
|
import type { ProjectConfig } from "../../types";
|
||||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||||
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
import { getPackageExecutionCommand } from "../../utils/package-runner";
|
||||||
|
|
||||||
export async function setupTauri(config: ProjectConfig): Promise<void> {
|
export async function setupTauri(config: ProjectConfig): Promise<void> {
|
||||||
const { packageManager, frontend, projectDir } = config;
|
const { packageManager, frontend, projectDir } = config;
|
||||||
|
|||||||
@@ -1,25 +1,11 @@
|
|||||||
import path from "node:path";
|
import { intro, log } from "@clack/prompts";
|
||||||
import {
|
|
||||||
cancel,
|
|
||||||
intro,
|
|
||||||
isCancel,
|
|
||||||
log,
|
|
||||||
outro,
|
|
||||||
select,
|
|
||||||
spinner,
|
|
||||||
} from "@clack/prompts";
|
|
||||||
import { consola } from "consola";
|
import { consola } from "consola";
|
||||||
import fs from "fs-extra";
|
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import { createCli, trpcServer, zod as z } from "trpc-cli";
|
import { createCli, trpcServer, zod as z } from "trpc-cli";
|
||||||
import { DEFAULT_CONFIG } from "./constants";
|
import {
|
||||||
import { addAddonsToProject } from "./helpers/project-generation/add-addons";
|
addAddonsHandler,
|
||||||
import { createProject } from "./helpers/project-generation/create-project";
|
createProjectHandler,
|
||||||
import { detectProjectConfig } from "./helpers/project-generation/detect-project-config";
|
} from "./helpers/project-generation/command-handlers";
|
||||||
import { getAddonsToAdd } from "./prompts/addons";
|
|
||||||
import { gatherConfig } from "./prompts/config-prompts";
|
|
||||||
import { getProjectName } from "./prompts/project-name";
|
|
||||||
import type { AddInput, CreateInput, ProjectConfig } from "./types";
|
|
||||||
import {
|
import {
|
||||||
AddonsSchema,
|
AddonsSchema,
|
||||||
APISchema,
|
APISchema,
|
||||||
@@ -33,282 +19,13 @@ import {
|
|||||||
ProjectNameSchema,
|
ProjectNameSchema,
|
||||||
RuntimeSchema,
|
RuntimeSchema,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { trackProjectCreation } from "./utils/analytics";
|
|
||||||
import { displayConfig } from "./utils/display-config";
|
|
||||||
import { generateReproducibleCommand } from "./utils/generate-reproducible-command";
|
|
||||||
import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
|
import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
|
||||||
import { openUrl } from "./utils/open-url";
|
import { openUrl } from "./utils/open-url";
|
||||||
import { renderTitle } from "./utils/render-title";
|
import { renderTitle } from "./utils/render-title";
|
||||||
import { displaySponsors, fetchSponsors } from "./utils/sponsors";
|
import { displaySponsors, fetchSponsors } from "./utils/sponsors";
|
||||||
import { getProvidedFlags, processAndValidateFlags } from "./validation";
|
|
||||||
|
|
||||||
const t = trpcServer.initTRPC.create();
|
const t = trpcServer.initTRPC.create();
|
||||||
|
|
||||||
async function handleDirectoryConflict(currentPathInput: string): Promise<{
|
|
||||||
finalPathInput: string;
|
|
||||||
shouldClearDirectory: boolean;
|
|
||||||
}> {
|
|
||||||
while (true) {
|
|
||||||
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
|
|
||||||
const dirExists = fs.pathExistsSync(resolvedPath);
|
|
||||||
const dirIsNotEmpty = dirExists && fs.readdirSync(resolvedPath).length > 0;
|
|
||||||
|
|
||||||
if (!dirIsNotEmpty) {
|
|
||||||
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
log.warn(
|
|
||||||
`Directory "${pc.yellow(
|
|
||||||
currentPathInput,
|
|
||||||
)}" already exists and is not empty.`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const action = await select<"overwrite" | "merge" | "rename" | "cancel">({
|
|
||||||
message: "What would you like to do?",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
value: "overwrite",
|
|
||||||
label: "Overwrite",
|
|
||||||
hint: "Empty the directory and create the project",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "merge",
|
|
||||||
label: "Merge",
|
|
||||||
hint: "Create project files inside, potentially overwriting conflicts",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "rename",
|
|
||||||
label: "Choose a different name/path",
|
|
||||||
hint: "Keep the existing directory and create a new one",
|
|
||||||
},
|
|
||||||
{ value: "cancel", label: "Cancel", hint: "Abort the process" },
|
|
||||||
],
|
|
||||||
initialValue: "rename",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isCancel(action)) {
|
|
||||||
cancel(pc.red("Operation cancelled."));
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case "overwrite":
|
|
||||||
return { finalPathInput: currentPathInput, shouldClearDirectory: true };
|
|
||||||
case "merge":
|
|
||||||
log.info(
|
|
||||||
`Proceeding into existing directory "${pc.yellow(
|
|
||||||
currentPathInput,
|
|
||||||
)}". Files may be overwritten.`,
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
finalPathInput: currentPathInput,
|
|
||||||
shouldClearDirectory: false,
|
|
||||||
};
|
|
||||||
case "rename": {
|
|
||||||
log.info("Please choose a different project name or path.");
|
|
||||||
const newPathInput = await getProjectName(undefined);
|
|
||||||
return await handleDirectoryConflict(newPathInput);
|
|
||||||
}
|
|
||||||
case "cancel":
|
|
||||||
cancel(pc.red("Operation cancelled."));
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setupProjectDirectory(
|
|
||||||
finalPathInput: string,
|
|
||||||
shouldClearDirectory: boolean,
|
|
||||||
): Promise<{ finalResolvedPath: string; finalBaseName: string }> {
|
|
||||||
let finalResolvedPath: string;
|
|
||||||
let finalBaseName: string;
|
|
||||||
|
|
||||||
if (finalPathInput === ".") {
|
|
||||||
finalResolvedPath = process.cwd();
|
|
||||||
finalBaseName = path.basename(finalResolvedPath);
|
|
||||||
} else {
|
|
||||||
finalResolvedPath = path.resolve(process.cwd(), finalPathInput);
|
|
||||||
finalBaseName = path.basename(finalResolvedPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldClearDirectory) {
|
|
||||||
const s = spinner();
|
|
||||||
s.start(`Clearing directory "${finalResolvedPath}"...`);
|
|
||||||
try {
|
|
||||||
await fs.emptyDir(finalResolvedPath);
|
|
||||||
s.stop(`Directory "${finalResolvedPath}" cleared.`);
|
|
||||||
} catch (error) {
|
|
||||||
s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
|
|
||||||
consola.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await fs.ensureDir(finalResolvedPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { finalResolvedPath, finalBaseName };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createProjectHandler(
|
|
||||||
input: CreateInput & { projectName?: string },
|
|
||||||
) {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
renderTitle();
|
|
||||||
intro(pc.magenta("Creating a new Better-T Stack project"));
|
|
||||||
|
|
||||||
let currentPathInput: string;
|
|
||||||
if (input.yes && input.projectName) {
|
|
||||||
currentPathInput = input.projectName;
|
|
||||||
} else if (input.yes) {
|
|
||||||
let defaultName = DEFAULT_CONFIG.relativePath;
|
|
||||||
let counter = 1;
|
|
||||||
while (
|
|
||||||
fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) &&
|
|
||||||
fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0
|
|
||||||
) {
|
|
||||||
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
|
|
||||||
counter++;
|
|
||||||
}
|
|
||||||
currentPathInput = defaultName;
|
|
||||||
} else {
|
|
||||||
currentPathInput = await getProjectName(input.projectName);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { finalPathInput, shouldClearDirectory } =
|
|
||||||
await handleDirectoryConflict(currentPathInput);
|
|
||||||
|
|
||||||
const { finalResolvedPath, finalBaseName } = await setupProjectDirectory(
|
|
||||||
finalPathInput,
|
|
||||||
shouldClearDirectory,
|
|
||||||
);
|
|
||||||
|
|
||||||
const cliInput = {
|
|
||||||
...input,
|
|
||||||
projectDirectory: input.projectName,
|
|
||||||
};
|
|
||||||
|
|
||||||
const providedFlags = getProvidedFlags(cliInput);
|
|
||||||
const flagConfig = processAndValidateFlags(
|
|
||||||
cliInput,
|
|
||||||
providedFlags,
|
|
||||||
finalBaseName,
|
|
||||||
);
|
|
||||||
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
|
|
||||||
|
|
||||||
if (!input.yes && Object.keys(otherFlags).length > 0) {
|
|
||||||
log.info(pc.yellow("Using these pre-selected options:"));
|
|
||||||
log.message(displayConfig(otherFlags));
|
|
||||||
log.message("");
|
|
||||||
}
|
|
||||||
|
|
||||||
let config: ProjectConfig;
|
|
||||||
if (input.yes) {
|
|
||||||
config = {
|
|
||||||
...DEFAULT_CONFIG,
|
|
||||||
...flagConfig,
|
|
||||||
projectName: finalBaseName,
|
|
||||||
projectDir: finalResolvedPath,
|
|
||||||
relativePath: finalPathInput,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (config.backend === "convex") {
|
|
||||||
log.info(
|
|
||||||
"Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo",
|
|
||||||
);
|
|
||||||
} else if (config.backend === "none") {
|
|
||||||
log.info(
|
|
||||||
"Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
pc.yellow("Using default/flag options (config prompts skipped):"),
|
|
||||||
);
|
|
||||||
log.message(displayConfig(config));
|
|
||||||
log.message("");
|
|
||||||
} else {
|
|
||||||
config = await gatherConfig(
|
|
||||||
flagConfig,
|
|
||||||
finalBaseName,
|
|
||||||
finalResolvedPath,
|
|
||||||
finalPathInput,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await createProject(config);
|
|
||||||
|
|
||||||
const reproducibleCommand = generateReproducibleCommand(config);
|
|
||||||
log.success(
|
|
||||||
pc.blue(
|
|
||||||
`You can reproduce this setup with the following command:\n${reproducibleCommand}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await trackProjectCreation(config);
|
|
||||||
|
|
||||||
const elapsedTimeInSeconds = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
||||||
outro(
|
|
||||||
pc.magenta(
|
|
||||||
`Project created successfully in ${pc.bold(
|
|
||||||
elapsedTimeInSeconds,
|
|
||||||
)} seconds!`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addAddonsHandler(input: AddInput): Promise<void> {
|
|
||||||
try {
|
|
||||||
if (!input.addons || input.addons.length === 0) {
|
|
||||||
const projectDir = input.projectDir || process.cwd();
|
|
||||||
const detectedConfig = await detectProjectConfig(projectDir);
|
|
||||||
|
|
||||||
if (!detectedConfig) {
|
|
||||||
cancel(
|
|
||||||
pc.red(
|
|
||||||
"Could not detect project configuration. Please ensure this is a valid Better-T Stack project.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const addonsPrompt = await getAddonsToAdd(
|
|
||||||
detectedConfig.frontend || [],
|
|
||||||
detectedConfig.addons || [],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (addonsPrompt.length === 0) {
|
|
||||||
outro(
|
|
||||||
pc.yellow(
|
|
||||||
"No addons to add or all compatible addons are already present.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.addons = addonsPrompt;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!input.addons || input.addons.length === 0) {
|
|
||||||
outro(pc.yellow("No addons specified to add."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await addAddonsToProject({
|
|
||||||
...input,
|
|
||||||
addons: input.addons,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = t.router({
|
const router = t.router({
|
||||||
init: t.procedure
|
init: t.procedure
|
||||||
.meta({
|
.meta({
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export async function trackProjectCreation(
|
|||||||
flushInterval: 0,
|
flushInterval: 0,
|
||||||
privacyMode: true,
|
privacyMode: true,
|
||||||
disableGeoip: true,
|
disableGeoip: true,
|
||||||
disabled: process.env.MODE !== "prod",
|
disabled: process.env.TELEMETRY !== "true",
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
113
apps/cli/src/utils/project-directory.ts
Normal file
113
apps/cli/src/utils/project-directory.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { cancel, isCancel, log, select, spinner } from "@clack/prompts";
|
||||||
|
import { consola } from "consola";
|
||||||
|
import fs from "fs-extra";
|
||||||
|
import pc from "picocolors";
|
||||||
|
import { getProjectName } from "../prompts/project-name";
|
||||||
|
|
||||||
|
export async function handleDirectoryConflict(
|
||||||
|
currentPathInput: string,
|
||||||
|
): Promise<{
|
||||||
|
finalPathInput: string;
|
||||||
|
shouldClearDirectory: boolean;
|
||||||
|
}> {
|
||||||
|
while (true) {
|
||||||
|
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
|
||||||
|
const dirExists = fs.pathExistsSync(resolvedPath);
|
||||||
|
const dirIsNotEmpty = dirExists && fs.readdirSync(resolvedPath).length > 0;
|
||||||
|
|
||||||
|
if (!dirIsNotEmpty) {
|
||||||
|
return { finalPathInput: currentPathInput, shouldClearDirectory: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn(
|
||||||
|
`Directory "${pc.yellow(
|
||||||
|
currentPathInput,
|
||||||
|
)}" already exists and is not empty.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const action = await select<"overwrite" | "merge" | "rename" | "cancel">({
|
||||||
|
message: "What would you like to do?",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "overwrite",
|
||||||
|
label: "Overwrite",
|
||||||
|
hint: "Empty the directory and create the project",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "merge",
|
||||||
|
label: "Merge",
|
||||||
|
hint: "Create project files inside, potentially overwriting conflicts",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "rename",
|
||||||
|
label: "Choose a different name/path",
|
||||||
|
hint: "Keep the existing directory and create a new one",
|
||||||
|
},
|
||||||
|
{ value: "cancel", label: "Cancel", hint: "Abort the process" },
|
||||||
|
],
|
||||||
|
initialValue: "rename",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isCancel(action)) {
|
||||||
|
cancel(pc.red("Operation cancelled."));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case "overwrite":
|
||||||
|
return { finalPathInput: currentPathInput, shouldClearDirectory: true };
|
||||||
|
case "merge":
|
||||||
|
log.info(
|
||||||
|
`Proceeding into existing directory "${pc.yellow(
|
||||||
|
currentPathInput,
|
||||||
|
)}". Files may be overwritten.`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
finalPathInput: currentPathInput,
|
||||||
|
shouldClearDirectory: false,
|
||||||
|
};
|
||||||
|
case "rename": {
|
||||||
|
log.info("Please choose a different project name or path.");
|
||||||
|
const newPathInput = await getProjectName(undefined);
|
||||||
|
return await handleDirectoryConflict(newPathInput);
|
||||||
|
}
|
||||||
|
case "cancel":
|
||||||
|
cancel(pc.red("Operation cancelled."));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupProjectDirectory(
|
||||||
|
finalPathInput: string,
|
||||||
|
shouldClearDirectory: boolean,
|
||||||
|
): Promise<{ finalResolvedPath: string; finalBaseName: string }> {
|
||||||
|
let finalResolvedPath: string;
|
||||||
|
let finalBaseName: string;
|
||||||
|
|
||||||
|
if (finalPathInput === ".") {
|
||||||
|
finalResolvedPath = process.cwd();
|
||||||
|
finalBaseName = path.basename(finalResolvedPath);
|
||||||
|
} else {
|
||||||
|
finalResolvedPath = path.resolve(process.cwd(), finalPathInput);
|
||||||
|
finalBaseName = path.basename(finalResolvedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldClearDirectory) {
|
||||||
|
const s = spinner();
|
||||||
|
s.start(`Clearing directory "${finalResolvedPath}"...`);
|
||||||
|
try {
|
||||||
|
await fs.emptyDir(finalResolvedPath);
|
||||||
|
s.stop(`Directory "${finalResolvedPath}" cleared.`);
|
||||||
|
} catch (error) {
|
||||||
|
s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
|
||||||
|
consola.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await fs.ensureDir(finalResolvedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { finalResolvedPath, finalBaseName };
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@ export default defineConfig({
|
|||||||
banner: "#!/usr/bin/env node",
|
banner: "#!/usr/bin/env node",
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
POSTHOG_API_KEY: "phc_8ZUxEwwfKMajJLvxz1daGd931dYbQrwKNficBmsdIrs",
|
POSTHOG_API_KEY: process.env.POSTHOG_API_KEY || "lol",
|
||||||
POSTHOG_HOST: "https://us.i.posthog.com",
|
POSTHOG_HOST: process.env.POSTHOG_HOST || "lool",
|
||||||
MODE: process.env.MODE || "dev", // wierd trick i know
|
TELEMETRY: process.env.TELEMETRY || "false", // wierd trick i know
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user