Skip to content

Commit

Permalink
feat(setup): allow for local setup only without connecting to the too…
Browse files Browse the repository at this point in the history
…ls (#51)
  • Loading branch information
Pmejna authored Dec 11, 2024
1 parent 2b00bb3 commit dc2c32e
Show file tree
Hide file tree
Showing 33 changed files with 424 additions and 136 deletions.
6 changes: 6 additions & 0 deletions .changeset/pink-steaks-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'stplr-core': patch
'stplr': patch
---

Add --no-deploy flag workflow
1 change: 1 addition & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"new-eggs-cheat",
"nine-berries-raise",
"old-radios-bake",
"pink-steaks-push",
"pretty-bags-travel",
"rare-beans-divide",
"selfish-cheetahs-vanish",
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# stplr

## 0.1.0-alpha.43

### Patch Changes

- Add --no-deploy flag workflow

## 0.1.0-alpha.42

### Minor Changes
Expand Down
47 changes: 47 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,50 @@ This command will guide you through the setup process and create a new project i
- [Next.js](https://nextjs.org/)
- [Supabase](https://supabase.io/)
- [Payload CMS](https://payloadcms.com/)

## Available Flags

### `-n, --name <name>`

**Description**: Set a specific name for the project being created. If not provided, the CLI will prompt you to enter a name.

**_Usage:_**:

```bash
npx stplr -n my-awesome-project
```

### `--no-deploy`

**Description**: Use this flag to set up the project locally without creating a GitHub repository, Supabase project, or deploying to Vercel. This is ideal if you want to scaffold the project and set up these integrations later.

**What Happens?**

- The project will be scaffolded in the specified (or current) directory.
- Required accounts (e.g., Supabase, Vercel) will not be linked during setup.
- You can resume the setup later to complete the integration steps.

**_Usage:_**

```bash
npx stplr --no-deploy
```

**To Resume Setup:**
Simply run `stplr` again in the project directory and select the unfinished project when prompted:

```bash
npx stplr
```

The CLI will detect that the project was not fully set up and guide you through the remaining steps.

### `--skip-payload`

**Description**: Skip adding Payload CMS to your project. Use this flag if you do not want Payload CMS to be part of your stack.

**_Usage_**:

```bash
npx stplr --skip-payload
```
13 changes: 13 additions & 0 deletions packages/cli/command-prompts/getProjectNamePrompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import inquirer from 'inquirer';

export const getProjectNamePrompt = async (): Promise<string> =>
(
await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'What is your project named?',
default: 'my-stapled-app',
},
])
).name;
4 changes: 4 additions & 0 deletions packages/cli/command-prompts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './getProjectNamePrompt';
export * from './overwriteDirectoryPrompt';
export * from './shouldUsePayloadPrompt';
export * from './unfinishedProjectsChoicePrompt';
19 changes: 19 additions & 0 deletions packages/cli/command-prompts/overwriteDirectoryPrompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import inquirer from 'inquirer';

/**
* Prompts the user to confirm whether they want to overwrite an existing project directory.
*
* @param projectName - The name of the project that already exists.
* @returns A promise that returns object with overwrite boolean value.
*
**/

export const overwriteDirectoryPrompt = async (projectName: string): Promise<{ overwrite: boolean }> =>
await inquirer.prompt([
{
type: 'confirm',
name: 'overwrite',
message: `The directory "${projectName}" already exists. Do you want to overwrite it?`,
default: false,
},
]);
19 changes: 19 additions & 0 deletions packages/cli/command-prompts/shouldUsePayloadPrompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import inquirer from 'inquirer';

/**
* Prompts the user to confirm whether they want to overwrite an existing project directory.
*
* @param projectName - The name of the project that already exists.
* @returns A promise that returns object with usePayload boolean value.
*
**/

export const shouldUsePayloadPrompt = async (): Promise<{ usePayload: boolean }> =>
await inquirer.prompt([
{
type: 'confirm',
name: 'usePayload',
message: 'Would you like to add Payload to your app?',
default: true,
},
]);
38 changes: 38 additions & 0 deletions packages/cli/command-prompts/unfinishedProjectsChoicePrompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import inquirer from 'inquirer';
import { ProjectChoice, UnfinishedProject } from '../utils/findUnfinishedProjects';

export type UnfinishedProjectsChoiceAnswers = {
resume: boolean;
unfinishedSelectedProject: UnfinishedProject;
};

/**
* Prompts the user to select an unfinished project to resume.
*
* @param unfinishedProjects - An array of unfinished projects.
* @param projectChoices - An array of project choices presented to the user.
* @returns A promise that returns object with resume boolean value and selected project.
*
**/

export const unfinishedProjectsChoice = async (
unfinishedProjects: UnfinishedProject[],
projectChoices: ProjectChoice[],
): Promise<UnfinishedProjectsChoiceAnswers> =>
await inquirer.prompt([
{
type: 'confirm',
name: 'resume',
message: `We found the following unfinished project(s):\n${unfinishedProjects
.map((p) => `- ${p.projectName}`)
.join('\n')}\nWould you like to resume one of them?`,
default: true,
},
{
type: 'list',
name: 'unfinishedSelectedProject',
message: 'Select a project to resume:',
choices: projectChoices,
when: (answers) => answers.resume && unfinishedProjects.length > 1,
},
]);
171 changes: 67 additions & 104 deletions packages/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,27 @@
import fs from 'fs';
import chalk from 'chalk';
import { Command } from 'commander';
import gradient from 'gradient-string';
import inquirer from 'inquirer';
import { createProject } from 'stplr-core';
import { checkAuthentication } from './utils/checkAuthentication';
import { checkTools } from './utils/checkTools';
import { findUnfinishedProjects, UnfinishedProject } from './utils/findUnfinishedProjects';

const asciiArt = `
.&&&% &&&&
.&&&% &&&&
.&&&&&&&&* (&&&&&&&&&&* (&&&.&&&&&&&&&& *&&&# &&&& #&&&&,
.&&&&((((, %&&&&(, .,#&&&&# (&&&&&%(**(%&&&&( *&&&# &&&& (&&&%
.&&&% %&&&. ,&&&% (&&&/ &&&&. *&&&# &&&& #&&&#
.&&&% ,&&&* (&&&. (&&&/ %&&&, *&&&# &&&&&&&&&&.
.&&&% %&&&. *&&&# (&&&/ %&&&, *&&&# &&&&&* *&&&%
.&&&% %&&&&%. ,&&&&&# (&&&/ %&&&, *&&&# &&&& #&&&/
.&&&% (&&&&&&&&&%* (&&&/ %&&&, *&&&# &&&& .&&&&,
`;

const displayHeader = () => {
const metalGradient = gradient([
{ color: '#3C3C3C', pos: 0 },
{ color: '#FFFFFF', pos: 1 },
]);

console.log(metalGradient(asciiArt));
console.log(chalk.bold('\nWelcome to Stapler!\n'));
};
import {
checkAuthentication,
checkTools,
displayHeader,
findUnfinishedProjects,
getProjectChoices,
UnfinishedProject,
} from './utils';
import {
getProjectNamePrompt,
overwriteDirectoryPrompt,
shouldUsePayloadPrompt,
unfinishedProjectsChoice,
} from './command-prompts';

interface Flags {
deploy?: boolean;
name?: string;
skipPayload?: boolean;
}

const program = new Command();

Expand All @@ -43,88 +36,68 @@ program
displayHeader();
})
.option('-n, --name <name>', 'Set the name of the project')
.option('--skip-payload', 'Skip adding Payload to the app')
.option('--resume', 'Resume an unfinished project');
.option(
'--no-deploy',
'Setup project locally without creating github repository, supabase project and vercel deployment',
)
.option('--skip-payload', 'Skip adding Payload to the app');

interface Flags {
name?: string;
skipPayload?: boolean;
}
program.parse(process.argv);

const createAction = async (options: Flags) => {
const shouldDeploy = options.deploy as boolean;
const currentDir = process.cwd();
const unfinishedProjects: UnfinishedProject[] = findUnfinishedProjects(currentDir);

let proceedWithNewProject = true;
let selectedProject: UnfinishedProject | null = null;

if (unfinishedProjects.length > 0) {
const projectChoices = unfinishedProjects.map((proj) => ({
name: proj.projectName,
value: proj,
}));

const resumeAnswers = await inquirer.prompt([
{
type: 'confirm',
name: 'resume',
message: `We found the following unfinished project(s):\n${unfinishedProjects
.map((p) => `- ${p.projectName}`)
.join('\n')}\nWould you like to resume one of them?`,
default: true,
},
{
type: 'list',
name: 'selectedProject',
message: 'Select a project to resume:',
choices: projectChoices,
when: (answers) => answers.resume && unfinishedProjects.length > 1,
},
]);
const unfinishedProjects: UnfinishedProject[] = findUnfinishedProjects(currentDir);

// If no project name is provided, and there are unfinished projects, we prompt the user to resume one of them
if (!options.name && unfinishedProjects.length > 0) {
const projectChoices = getProjectChoices(unfinishedProjects);

if (resumeAnswers.resume) {
const { resume, unfinishedSelectedProject } = await unfinishedProjectsChoice(unfinishedProjects, projectChoices);

if (resume) {
proceedWithNewProject = false;
if (unfinishedProjects.length === 1) {
selectedProject = unfinishedProjects[0];
} else {
selectedProject = resumeAnswers.selectedProject || null;
selectedProject = unfinishedSelectedProject || null;
}
}
}

// resume selected project
if (!proceedWithNewProject && selectedProject) {
process.chdir(selectedProject.projectPath);
selectedProject.state.options.name = selectedProject.projectName;
await createProject(selectedProject.state.options, selectedProject.projectPath).catch((error) => {
console.error('Error resuming project:', error);
});

if (shouldDeploy) {
await checkAuthentication();
await checkTools();
}

await createProject({ ...selectedProject.state.options, shouldDeploy }, selectedProject.projectPath).catch(
(error) => {
console.error('Error resuming project:', error);
},
);
} else {
// Create new project
const projectName =
options.name ||
(
await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'What is your project named?',
default: 'my-stapled-app',
},
])
).name;
options.name &&
console.log('You have provided a project name of:', chalk.yellow(options.name), "let's continue...");

const projectName = options.name || (await getProjectNamePrompt());

const projectDir = `${currentDir}/${projectName}`;
// Check if the directory already exists
if (fs.existsSync(projectDir)) {
const overwriteAnswer = await inquirer.prompt([
{
type: 'confirm',
name: 'overwrite',
message: `The directory "${projectName}" already exists. Do you want to overwrite it?`,
default: false,
},
]);

if (!overwriteAnswer.overwrite) {
const directoryExists = fs.existsSync(projectDir);

if (directoryExists) {
const shouldOverwrite = (await overwriteDirectoryPrompt(projectName)).overwrite;

if (shouldOverwrite === false) {
console.log(chalk.red('Project creation canceled. Project directory already exists.'));
return;
}
Expand All @@ -135,21 +108,13 @@ const createAction = async (options: Flags) => {
}

// Skip Payload if specified by the flag
const payloadAnswer = options.skipPayload
? { usePayload: false }
: await inquirer.prompt([
{
type: 'confirm',
name: 'usePayload',
message: 'Would you like to add Payload to your app?',
default: true,
},
]);

const finalOptions = { name: projectName, ...payloadAnswer };

await checkAuthentication();
await checkTools();
const payloadAnswer = options.skipPayload ? { usePayload: false } : await shouldUsePayloadPrompt();

const finalOptions = { name: projectName, shouldDeploy, ...payloadAnswer };
if (shouldDeploy) {
await checkAuthentication();
await checkTools();
}

await createProject(finalOptions, projectDir).catch((error) => {
console.error(chalk.red('Error creating project:', error));
Expand All @@ -162,8 +127,6 @@ program
.description(
'CLI tool to bootstrap an app with a variety of integrated steps. This tool guides you through the entire process of initializing, configuring, and deploying a new project.',
)
.option('-n, --name <name>', 'Set the name of the project')
.option('--skip-payload', 'Skip adding Payload to the app')
.action(createAction);
.action(() => createAction(program.opts()));

program.parse();
Loading

0 comments on commit dc2c32e

Please sign in to comment.