scaffly-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # Scaffly
2
+
3
+ [![npm version](https://img.shields.io/npm/v/scaffly.svg?style=flat-square)](https://www.npmjs.com/package/scaffly)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT)
5
+ [![GitHub Stars](https://img.shields.io/github/stars/ViniLF/scaffly?style=flat-square)](https://github.com/ViniLF/scaffly/stargazers)
6
+ [![Node.js Version](https://img.shields.io/node/v/scaffly.svg?style=flat-square)](https://nodejs.org)
7
+
8
+ > A fast, interactive CLI tool for scaffolding modern web and Node.js projects.
9
+
10
+ ---
11
+
12
+ ## Features
13
+
14
+ - **Interactive prompts** — beautiful terminal UI powered by `@clack/prompts`
15
+ - **4 supported stacks** — Next.js, React + Vite, Express, and Fastify
16
+ - **Optional extras** — pick and choose: ESLint, Prettier, Husky, Tailwind CSS, Docker, GitHub Actions
17
+ - **Production-ready boilerplate** — sensible defaults, clean folder structure
18
+ - **Zero config required** — just run and go
19
+
20
+ ---
21
+
22
+ ## Usage
23
+
24
+ Run without installing (always uses the latest version):
25
+
26
+ ```bash
27
+ npx scaffly
28
+ ```
29
+
30
+ Or install globally for repeated use:
31
+
32
+ ```bash
33
+ npm install -g scaffly
34
+ scaffly
35
+ ```
36
+
37
+ ### What happens next
38
+
39
+ 1. Enter your **project name**
40
+ 2. Choose a **stack**
41
+ 3. Pick any **extras**
42
+ 4. Scaffly creates the project, installs dependencies, and configures your tooling
43
+
44
+ ---
45
+
46
+ ## Supported Stacks
47
+
48
+ | Stack | Language | Best For |
49
+ |---|---|---|
50
+ | **Next.js** | React / JSX | Full-stack web applications, SSR, SSG |
51
+ | **React + Vite** | React / JSX | SPAs, client-side frontends |
52
+ | **Node.js + Express** | JavaScript (ESM) | REST APIs, microservices |
53
+ | **Fastify** | JavaScript (ESM) | High-performance APIs |
54
+
55
+ ---
56
+
57
+ ## Extras
58
+
59
+ | Extra | Description | Stacks |
60
+ |---|---|---|
61
+ | **ESLint + Prettier** | Linting and code formatting with zero-config defaults | All |
62
+ | **Husky + lint-staged** | Pre-commit hooks that run linting before each commit | All |
63
+ | **Tailwind CSS** | Utility-first CSS framework with PostCSS | Next.js, Vite |
64
+ | **Docker** | `Dockerfile` + `docker-compose.yml` optimised per stack | All |
65
+ | **GitHub Actions CI/CD** | Workflow that runs lint, test, and build on push/PR | All |
66
+
67
+ ---
68
+
69
+ ## Generated Project Structure
70
+
71
+ ### Next.js
72
+ ```
73
+ my-app/
74
+ ├── app/
75
+ │ ├── layout.js
76
+ │ ├── page.js
77
+ │ └── globals.css
78
+ ├── next.config.mjs
79
+ ├── package.json
80
+ └── .gitignore
81
+ ```
82
+
83
+ ### React + Vite
84
+ ```
85
+ my-app/
86
+ ├── src/
87
+ │ ├── App.jsx
88
+ │ ├── App.css
89
+ │ ├── main.jsx
90
+ │ └── index.css
91
+ ├── index.html
92
+ ├── vite.config.js
93
+ ├── package.json
94
+ └── .gitignore
95
+ ```
96
+
97
+ ### Express / Fastify
98
+ ```
99
+ my-app/
100
+ ├── src/
101
+ │ ├── index.js
102
+ │ ├── routes/
103
+ │ └── middleware/ (Express only)
104
+ ├── .env
105
+ ├── .env.example
106
+ ├── package.json
107
+ └── .gitignore
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Requirements
113
+
114
+ - **Node.js** `>= 18.0.0`
115
+ - **npm** `>= 8.0.0`
116
+
117
+ ---
118
+
119
+ ## Contributing
120
+
121
+ Contributions are welcome! Here's how to get started:
122
+
123
+ 1. **Fork** the repository
124
+ 2. **Clone** your fork: `git clone https://github.com/ViniLF/scaffly.git`
125
+ 3. **Install** dependencies: `npm install`
126
+ 4. **Create** a feature branch: `git checkout -b feat/your-feature`
127
+ 5. **Make** your changes and test them: `node bin/scaffly.js`
128
+ 6. **Commit** using [Conventional Commits](https://www.conventionalcommits.org/): `git commit -m "feat: add awesome feature"`
129
+ 7. **Push** to your branch: `git push origin feat/your-feature`
130
+ 8. **Open** a Pull Request
131
+
132
+ ### Adding a new stack
133
+
134
+ 1. Create `src/generators/your-stack.js` and export a `generate(projectName, projectPath)` function
135
+ 2. Register it in `bin/scaffly.js` under `GENERATORS`
136
+ 3. Add a prompt option in `src/prompts/index.js`
137
+ 4. Update this README
138
+
139
+ ### Adding a new extra
140
+
141
+ 1. Create `src/extras/your-extra.js` and export an `apply(projectPath, stack)` function
142
+ 2. Register it in `bin/scaffly.js` under `EXTRAS`
143
+ 3. Add a prompt option in `src/prompts/index.js`
144
+ 4. Update this README
145
+
146
+ ---
147
+
148
+ ## License
149
+
150
+ [MIT](./LICENSE) © ViniLF
package/bin/scaffly.js ADDED
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Scaffly — CLI entry point
5
+ *
6
+ * Flow:
7
+ * 1. Display intro banner
8
+ * 2. Collect project configuration via interactive prompts
9
+ * 3. Validate the target directory doesn't already exist
10
+ * 4. Run the selected stack generator (creates files + installs base deps)
11
+ * 5. Apply each selected extra (adds tooling on top)
12
+ * 6. Display a success message with next steps
13
+ */
14
+
15
+ import * as p from '@clack/prompts';
16
+ import chalk from 'chalk';
17
+ import path from 'path';
18
+ import fse from 'fs-extra';
19
+
20
+ import { collectAnswers } from '../src/prompts/index.js';
21
+ import { generate as generateNextjs } from '../src/generators/nextjs.js';
22
+ import { generate as generateVite } from '../src/generators/vite.js';
23
+ import { generate as generateExpress } from '../src/generators/express.js';
24
+ import { generate as generateFastify } from '../src/generators/fastify.js';
25
+ import { apply as applyEslint } from '../src/extras/eslint.js';
26
+ import { apply as applyHusky } from '../src/extras/husky.js';
27
+ import { apply as applyTailwind } from '../src/extras/tailwind.js';
28
+ import { apply as applyDocker } from '../src/extras/docker.js';
29
+ import { apply as applyGithubActions } from '../src/extras/github-actions.js';
30
+
31
+ // ── Registry maps ─────────────────────────────────────────────────────────────
32
+
33
+ const GENERATORS = {
34
+ nextjs: generateNextjs,
35
+ vite: generateVite,
36
+ express: generateExpress,
37
+ fastify: generateFastify,
38
+ };
39
+
40
+ const EXTRAS = {
41
+ eslint: applyEslint,
42
+ husky: applyHusky,
43
+ tailwind: applyTailwind,
44
+ docker: applyDocker,
45
+ 'github-actions': applyGithubActions,
46
+ };
47
+
48
+ const STACK_LABELS = {
49
+ nextjs: 'Next.js',
50
+ vite: 'React + Vite',
51
+ express: 'Node.js + Express',
52
+ fastify: 'Fastify',
53
+ };
54
+
55
+ const DEV_COMMANDS = {
56
+ nextjs: 'npm run dev',
57
+ vite: 'npm run dev',
58
+ express: 'npm run dev',
59
+ fastify: 'npm run dev',
60
+ };
61
+
62
+ // ── Main ──────────────────────────────────────────────────────────────────────
63
+
64
+ async function main() {
65
+ console.clear();
66
+
67
+ // Welcome banner
68
+ p.intro(
69
+ `${chalk.bgCyan.black(' scaffly ')} ${chalk.dim('— scaffold modern projects in seconds')}`
70
+ );
71
+
72
+ // ── Step 1: Collect answers ───────────────────────────────────────────────
73
+ const { projectName, stack, extras } = await collectAnswers();
74
+
75
+ const projectPath = path.resolve(process.cwd(), projectName);
76
+
77
+ // ── Step 2: Guard against existing directory ──────────────────────────────
78
+ if (await fse.pathExists(projectPath)) {
79
+ p.cancel(
80
+ `Directory ${chalk.cyan(projectName)} already exists. Choose a different project name.`
81
+ );
82
+ process.exit(1);
83
+ }
84
+
85
+ const s = p.spinner();
86
+
87
+ // ── Step 3: Generate base project ────────────────────────────────────────
88
+ s.start(`Scaffolding ${chalk.cyan(STACK_LABELS[stack])} project...`);
89
+
90
+ try {
91
+ await GENERATORS[stack](projectName, projectPath);
92
+ } catch (err) {
93
+ s.stop(chalk.red('Failed to scaffold project'));
94
+ p.cancel(`Generator error: ${err.message}`);
95
+ process.exit(1);
96
+ }
97
+
98
+ // ── Step 4: Apply extras ──────────────────────────────────────────────────
99
+ for (const extra of extras) {
100
+ s.message(`Applying ${chalk.cyan(extra)}...`);
101
+
102
+ try {
103
+ await EXTRAS[extra](projectPath, stack);
104
+ } catch (err) {
105
+ // Extras are non-fatal — warn and continue
106
+ s.message(chalk.yellow(`Warning: ${extra} could not be fully applied`));
107
+ p.log.warn(`${extra} failed: ${err.message}`);
108
+ }
109
+ }
110
+
111
+ s.stop(chalk.green('Done!'));
112
+
113
+ // ── Step 5: Show next steps ───────────────────────────────────────────────
114
+ const note = buildNextSteps(projectName, stack, extras);
115
+ p.note(note, chalk.bold('Next steps'));
116
+
117
+ p.outro(
118
+ `Built something cool? Give Scaffly a star on GitHub! ${chalk.dim('github.com/ViniLF/scaffly')}`
119
+ );
120
+ }
121
+
122
+ /**
123
+ * Builds the formatted "next steps" message shown after a successful scaffold.
124
+ *
125
+ * @param {string} projectName
126
+ * @param {string} stack
127
+ * @param {string[]} extras
128
+ * @returns {string}
129
+ */
130
+ function buildNextSteps(projectName, stack, extras) {
131
+ const steps = [
132
+ `${chalk.dim('1.')} cd ${chalk.cyan(projectName)}`,
133
+ `${chalk.dim('2.')} ${chalk.cyan(DEV_COMMANDS[stack])}`,
134
+ ];
135
+
136
+ if (stack === 'nextjs' || stack === 'vite') {
137
+ steps.push(`${chalk.dim('3.')} Open ${chalk.cyan('http://localhost:3000')}`);
138
+ } else {
139
+ steps.push(`${chalk.dim('3.')} Test: ${chalk.cyan('curl http://localhost:3000/api/health')}`);
140
+ }
141
+
142
+ if (extras.includes('husky')) {
143
+ steps.push('');
144
+ steps.push(
145
+ `${chalk.dim('Tip:')} Run ${chalk.cyan('git init && npm run prepare')} to activate Husky hooks`
146
+ );
147
+ }
148
+
149
+ if (extras.includes('docker')) {
150
+ steps.push('');
151
+ steps.push(`${chalk.dim('Docker:')} ${chalk.cyan('docker compose up --build')}`);
152
+ }
153
+
154
+ return steps.join('\n');
155
+ }
156
+
157
+ // ── Run ───────────────────────────────────────────────────────────────────────
158
+
159
+ main().catch((err) => {
160
+ console.error(chalk.red('\nUnexpected error:'), err);
161
+ process.exit(1);
162
+ });
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "scaffly-cli",
3
+ "version": "1.0.0",
4
+ "description": "A fast, interactive CLI tool for scaffolding modern web and Node.js projects",
5
+ "type": "module",
6
+ "bin": {
7
+ "scaffly": "./bin/scaffly.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/scaffly.js",
11
+ "test": "echo \"No tests yet\" && exit 0"
12
+ },
13
+ "keywords": [
14
+ "cli",
15
+ "scaffold",
16
+ "boilerplate",
17
+ "nextjs",
18
+ "react",
19
+ "vite",
20
+ "express",
21
+ "fastify"
22
+ ],
23
+ "author": "ViniLF",
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "@clack/prompts": "^0.7.0",
27
+ "chalk": "^5.3.0",
28
+ "execa": "^9.3.0",
29
+ "fs-extra": "^11.2.0"
30
+ },
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ }
34
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Scaffly — Docker extra
3
+ * Adds a Dockerfile, docker-compose.yml, and .dockerignore tailored to the stack.
4
+ *
5
+ * Strategy:
6
+ * - Next.js / Vite: multi-stage build (build → serve)
7
+ * - Express / Fastify: single-stage Node.js image (production deps only)
8
+ */
9
+
10
+ import path from 'path';
11
+ import { writeFile } from '../utils/index.js';
12
+
13
+ // ── Dockerfiles per stack ─────────────────────────────────────────────────────
14
+
15
+ const DOCKERFILES = {
16
+ nextjs: `# ── Stage 1: Install dependencies ────────────────────────────────────────
17
+ FROM node:20-alpine AS deps
18
+ WORKDIR /app
19
+ COPY package*.json ./
20
+ RUN npm ci
21
+
22
+ # ── Stage 2: Build the application ────────────────────────────────────────
23
+ FROM node:20-alpine AS builder
24
+ WORKDIR /app
25
+ COPY --from=deps /app/node_modules ./node_modules
26
+ COPY . .
27
+ RUN npm run build
28
+
29
+ # ── Stage 3: Production image ──────────────────────────────────────────────
30
+ FROM node:20-alpine AS runner
31
+ WORKDIR /app
32
+
33
+ ENV NODE_ENV=production
34
+
35
+ # Copy only what's needed to run the app
36
+ COPY --from=builder /app/public ./public
37
+ COPY --from=builder /app/.next/standalone ./
38
+ COPY --from=builder /app/.next/static ./.next/static
39
+
40
+ EXPOSE 3000
41
+ ENV PORT=3000
42
+
43
+ CMD ["node", "server.js"]
44
+ `,
45
+
46
+ vite: `# ── Stage 1: Build the app ────────────────────────────────────────────────
47
+ FROM node:20-alpine AS builder
48
+ WORKDIR /app
49
+ COPY package*.json ./
50
+ RUN npm ci
51
+ COPY . .
52
+ RUN npm run build
53
+
54
+ # ── Stage 2: Serve with Nginx ──────────────────────────────────────────────
55
+ FROM nginx:alpine AS runner
56
+ COPY --from=builder /app/dist /usr/share/nginx/html
57
+
58
+ # Optional: custom Nginx config for SPA routing
59
+ # COPY nginx.conf /etc/nginx/conf.d/default.conf
60
+
61
+ EXPOSE 80
62
+
63
+ CMD ["nginx", "-g", "daemon off;"]
64
+ `,
65
+
66
+ express: `FROM node:20-alpine
67
+ WORKDIR /app
68
+
69
+ # Install production dependencies only
70
+ COPY package*.json ./
71
+ RUN npm ci --omit=dev
72
+
73
+ # Copy application source
74
+ COPY . .
75
+
76
+ EXPOSE 3000
77
+ ENV NODE_ENV=production
78
+
79
+ CMD ["npm", "start"]
80
+ `,
81
+
82
+ fastify: `FROM node:20-alpine
83
+ WORKDIR /app
84
+
85
+ # Install production dependencies only
86
+ COPY package*.json ./
87
+ RUN npm ci --omit=dev
88
+
89
+ # Copy application source
90
+ COPY . .
91
+
92
+ EXPOSE 3000
93
+ ENV NODE_ENV=production
94
+
95
+ CMD ["npm", "start"]
96
+ `,
97
+ };
98
+
99
+ // ── docker-compose.yml per stack ─────────────────────────────────────────────
100
+
101
+ const COMPOSE_FILES = {
102
+ nextjs: `services:
103
+ app:
104
+ build: .
105
+ ports:
106
+ - "3000:3000"
107
+ environment:
108
+ - NODE_ENV=production
109
+ restart: unless-stopped
110
+ `,
111
+
112
+ vite: `services:
113
+ app:
114
+ build: .
115
+ ports:
116
+ - "80:80"
117
+ restart: unless-stopped
118
+ `,
119
+
120
+ express: `services:
121
+ app:
122
+ build: .
123
+ ports:
124
+ - "3000:3000"
125
+ environment:
126
+ - NODE_ENV=production
127
+ - PORT=3000
128
+ restart: unless-stopped
129
+ `,
130
+
131
+ fastify: `services:
132
+ app:
133
+ build: .
134
+ ports:
135
+ - "3000:3000"
136
+ environment:
137
+ - NODE_ENV=production
138
+ - PORT=3000
139
+ restart: unless-stopped
140
+ `,
141
+ };
142
+
143
+ // ── .dockerignore (shared across stacks) ──────────────────────────────────────
144
+
145
+ const DOCKERIGNORE = `# Source control
146
+ .git
147
+ .gitignore
148
+
149
+ # Dependencies (will be re-installed inside the container)
150
+ node_modules
151
+
152
+ # Build output (re-built inside the container)
153
+ dist
154
+ build
155
+ .next
156
+
157
+ # Environment files — never bake secrets into an image
158
+ .env
159
+ .env.local
160
+ .env.*.local
161
+
162
+ # Dev tooling
163
+ .eslintrc*
164
+ .prettierrc*
165
+ .husky
166
+
167
+ # OS artifacts
168
+ .DS_Store
169
+ Thumbs.db
170
+
171
+ # Docs
172
+ README.md
173
+ `;
174
+
175
+ /**
176
+ * Adds Docker support files to the project.
177
+ *
178
+ * @param {string} projectPath - Absolute path to the project root
179
+ * @param {string} stack - Selected stack identifier
180
+ */
181
+ export async function apply(projectPath, stack) {
182
+ await writeFile(path.join(projectPath, 'Dockerfile'), DOCKERFILES[stack]);
183
+ await writeFile(path.join(projectPath, 'docker-compose.yml'), COMPOSE_FILES[stack]);
184
+ await writeFile(path.join(projectPath, '.dockerignore'), DOCKERIGNORE);
185
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Scaffly — ESLint + Prettier extra
3
+ * Adds code linting (ESLint) and formatting (Prettier) to the generated project.
4
+ * Config is tailored to the selected stack.
5
+ */
6
+
7
+ import path from 'path';
8
+ import { writeFile, writeJson, updatePackageJson, runCommand } from '../utils/index.js';
9
+
10
+ // ── ESLint configs per stack ──────────────────────────────────────────────────
11
+
12
+ const ESLINT_CONFIGS = {
13
+ nextjs: {
14
+ env: { browser: true, es2021: true, node: true },
15
+ extends: ['next/core-web-vitals', 'plugin:prettier/recommended'],
16
+ rules: {},
17
+ },
18
+ vite: {
19
+ env: { browser: true, es2021: true },
20
+ extends: [
21
+ 'eslint:recommended',
22
+ 'plugin:react/recommended',
23
+ 'plugin:react-hooks/recommended',
24
+ 'plugin:prettier/recommended',
25
+ ],
26
+ settings: {
27
+ react: { version: 'detect' },
28
+ },
29
+ rules: {
30
+ 'react/react-in-jsx-scope': 'off', // Not needed in React 17+
31
+ 'react/prop-types': 'off',
32
+ },
33
+ },
34
+ express: {
35
+ env: { node: true, es2021: true },
36
+ extends: ['eslint:recommended', 'plugin:prettier/recommended'],
37
+ parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
38
+ rules: {},
39
+ },
40
+ fastify: {
41
+ env: { node: true, es2021: true },
42
+ extends: ['eslint:recommended', 'plugin:prettier/recommended'],
43
+ parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
44
+ rules: {},
45
+ },
46
+ };
47
+
48
+ // ── Packages to install per stack ─────────────────────────────────────────────
49
+
50
+ const PACKAGES = {
51
+ nextjs: [
52
+ 'eslint',
53
+ 'eslint-config-next',
54
+ 'prettier',
55
+ 'eslint-config-prettier',
56
+ 'eslint-plugin-prettier',
57
+ ],
58
+ vite: [
59
+ 'eslint',
60
+ 'prettier',
61
+ 'eslint-config-prettier',
62
+ 'eslint-plugin-prettier',
63
+ 'eslint-plugin-react',
64
+ 'eslint-plugin-react-hooks',
65
+ ],
66
+ express: ['eslint', 'prettier', 'eslint-config-prettier', 'eslint-plugin-prettier'],
67
+ fastify: ['eslint', 'prettier', 'eslint-config-prettier', 'eslint-plugin-prettier'],
68
+ };
69
+
70
+ // ── Lint scripts per stack ────────────────────────────────────────────────────
71
+
72
+ const LINT_SCRIPTS = {
73
+ nextjs: {
74
+ lint: 'next lint',
75
+ 'lint:fix': 'next lint --fix',
76
+ },
77
+ vite: {
78
+ lint: 'eslint . --ext .js,.jsx --max-warnings 0',
79
+ 'lint:fix': 'eslint . --ext .js,.jsx --fix',
80
+ },
81
+ express: {
82
+ lint: 'eslint . --ext .js --max-warnings 0',
83
+ 'lint:fix': 'eslint . --ext .js --fix',
84
+ },
85
+ fastify: {
86
+ lint: 'eslint . --ext .js --max-warnings 0',
87
+ 'lint:fix': 'eslint . --ext .js --fix',
88
+ },
89
+ };
90
+
91
+ /**
92
+ * Applies ESLint + Prettier configuration to the project.
93
+ *
94
+ * @param {string} projectPath - Absolute path to the project root
95
+ * @param {string} stack - Selected stack identifier
96
+ */
97
+ export async function apply(projectPath, stack) {
98
+ // Write .eslintrc.json
99
+ await writeJson(path.join(projectPath, '.eslintrc.json'), ESLINT_CONFIGS[stack]);
100
+
101
+ // Write .prettierrc
102
+ await writeJson(path.join(projectPath, '.prettierrc'), {
103
+ semi: true,
104
+ singleQuote: true,
105
+ tabWidth: 2,
106
+ trailingComma: 'es5',
107
+ printWidth: 80,
108
+ arrowParens: 'always',
109
+ });
110
+
111
+ // Write .eslintignore
112
+ await writeFile(
113
+ path.join(projectPath, '.eslintignore'),
114
+ `node_modules
115
+ dist
116
+ build
117
+ .next
118
+ out
119
+ `
120
+ );
121
+
122
+ // Write .prettierignore
123
+ await writeFile(
124
+ path.join(projectPath, '.prettierignore'),
125
+ `node_modules
126
+ dist
127
+ build
128
+ .next
129
+ out
130
+ package-lock.json
131
+ `
132
+ );
133
+
134
+ // Add lint and format scripts to package.json
135
+ await updatePackageJson(projectPath, {
136
+ scripts: {
137
+ ...LINT_SCRIPTS[stack],
138
+ format: 'prettier --write .',
139
+ 'format:check': 'prettier --check .',
140
+ },
141
+ });
142
+
143
+ // Install ESLint + Prettier packages as dev dependencies
144
+ await runCommand('npm', ['install', '--save-dev', ...PACKAGES[stack]], projectPath);
145
+ }