@raulmoracode/create 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/LICENSE +21 -0
- package/README.md +231 -0
- package/dist/cli/args.js +34 -0
- package/dist/cli/output.js +20 -0
- package/dist/cli/run.js +326 -0
- package/dist/config/agents.js +224 -0
- package/dist/config/biome.js +30 -0
- package/dist/config/branding.js +2 -0
- package/dist/config/commitlint.js +8 -0
- package/dist/config/components.js +36 -0
- package/dist/config/editorconfig.js +14 -0
- package/dist/config/husky.js +14 -0
- package/dist/config/npmrc.js +3 -0
- package/dist/config/nvmrc.js +4 -0
- package/dist/config/pnpm-workspace.js +85 -0
- package/dist/config/query.js +16 -0
- package/dist/config/tailwind.js +31 -0
- package/dist/config/tech.js +83 -0
- package/dist/config/testing.js +26 -0
- package/dist/config/vscode.js +39 -0
- package/dist/frameworks/index.js +9 -0
- package/dist/frameworks/next.js +115 -0
- package/dist/frameworks/types.js +1 -0
- package/dist/frameworks/vite.js +135 -0
- package/dist/generators/configure-biome.js +22 -0
- package/dist/generators/configure-git-hooks.js +11 -0
- package/dist/generators/configure-node.js +45 -0
- package/dist/generators/configure-project.js +233 -0
- package/dist/generators/configure-registry.js +5 -0
- package/dist/generators/configure-shadcn.js +6 -0
- package/dist/generators/configure-testing.js +6 -0
- package/dist/generators/configure-vscode.js +6 -0
- package/dist/generators/create-project.js +3 -0
- package/dist/git/commit.js +12 -0
- package/dist/git/init.js +11 -0
- package/dist/git/push.js +59 -0
- package/dist/git/remote.js +30 -0
- package/dist/index.js +16 -0
- package/dist/prompts/cancel.js +8 -0
- package/dist/prompts/confirm.js +11 -0
- package/dist/prompts/framework.js +25 -0
- package/dist/prompts/github.js +25 -0
- package/dist/prompts/project.js +22 -0
- package/dist/prompts/tech.js +18 -0
- package/dist/utils/exec.js +107 -0
- package/dist/utils/filesystem.js +108 -0
- package/dist/utils/registry.js +18 -0
- package/dist/utils/validation.js +153 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 raulmoracode
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# @raulmoracode/create
|
|
2
|
+
|
|
3
|
+
Professional CLI to scaffold modern React projects. Built with Node.js, TypeScript, [Clack](https://github.com/natemoo-re/clack) and pnpm.
|
|
4
|
+
|
|
5
|
+
- **Package:** [`@raulmoracode/create`](https://www.npmjs.com/package/@raulmoracode/create) (published to npmjs)
|
|
6
|
+
- **Repository:** [`raulmoracode/raulmoracode-create`](https://github.com/raulmoracode/raulmoracode-create)
|
|
7
|
+
- **Command:** `raulmoracode-create`
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- [Node.js](https://nodejs.org) 24 LTS
|
|
12
|
+
- [pnpm](https://pnpm.io/installation) (declared per project via `packageManager`)
|
|
13
|
+
- npm
|
|
14
|
+
- [Git](https://git-scm.com/downloads) with a configured identity (`user.name` and `user.email`)
|
|
15
|
+
- A GitHub account and an existing empty repository for the new project
|
|
16
|
+
- `GH_TOKEN` (optional): only needed to install the private `@raulmoracode/icons` package. Without registry access, the project is generated without `.npmrc` and without icons — everything else is identical.
|
|
17
|
+
- [VS Code](https://code.visualstudio.com) (optional, used to open the project at the end)
|
|
18
|
+
|
|
19
|
+
## Install globally
|
|
20
|
+
|
|
21
|
+
The package is published to [npmjs](https://www.npmjs.com/package/@raulmoracode/create). No authentication needed.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g @raulmoracode/create
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Verify:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
raulmoracode-create
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
raulmoracode-create
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The CLI guides you through the whole process with Clack prompts:
|
|
40
|
+
|
|
41
|
+
1. Select the technology: **React + Vite** or **Next.js**.
|
|
42
|
+
2. Select the technologies to include (everything preselected by default).
|
|
43
|
+
3. Enter the project name.
|
|
44
|
+
4. Enter the GitHub repository URL (the repository must already exist).
|
|
45
|
+
5. The project is created with the official generators (`create-vite` / `create-next-app`) and fully configured.
|
|
46
|
+
6. Git is initialized, the remote is configured, the initial commit is created and pushed to GitHub.
|
|
47
|
+
7. Optionally open the project with VS Code.
|
|
48
|
+
|
|
49
|
+
Available flags:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
raulmoracode-create --verbose # print the output of every external command
|
|
53
|
+
raulmoracode-create --help # show help
|
|
54
|
+
raulmoracode-create --version # show the installed version
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## What the CLI does
|
|
58
|
+
|
|
59
|
+
- Scaffolds with the **official generators** (never a hand-written template):
|
|
60
|
+
- React + Vite: `pnpm create vite@9.2.1 <name> --template react-ts` (the `create-vite` release line that scaffolds the Vite 8.x line; `create-vite@8.3.1` does not exist), then pins `vite@8.3.1`, `react@19.3.0` and `typescript@7.0.2`.
|
|
61
|
+
- Next.js: `pnpm create next-app@16.3.6 <name> --ts --app --src-dir --import-alias "@/*" --biome --use-pnpm --disable-git --yes`.
|
|
62
|
+
- Installs dependencies with **pnpm** and pins exact versions (no `^` or `~`).
|
|
63
|
+
- Configures **Tailwind CSS 4.3.3** (CSS-first configuration, `@tailwindcss/vite` for Vite and `@tailwindcss/postcss` for Next.js).
|
|
64
|
+
- Prepares **shadcn**: writes a complete `components.json` (including the `@raulmoracode` registry) and the `cn()` helper (`src/lib/utils.ts`). No components are preinstalled; add them later with `pnpm dlx shadcn@latest add <component>`.
|
|
65
|
+
- Configures the private **`@raulmoracode` registry** in `.npmrc` using `${GH_TOKEN}` (never a real token) — but only when registry access is detected; otherwise `.npmrc` and `@raulmoracode/icons` are skipped.
|
|
66
|
+
- Installs **Zustand 5.0.15**, **React Hook Form 7.89.0**, **Zod 4.6.5**, **TanStack Query 5.104.0** and **@raulmoracode/icons 1.1.0**, and wires the TanStack Query provider.
|
|
67
|
+
- Configures **Biome 2.5.14** (formatter, linter and organize imports) and removes the ESLint/Oxlint leftovers from the official templates.
|
|
68
|
+
- Configures **Vitest 5.0.2**, **@testing-library/react 16.3.3** and **@testing-library/dom 10.4.2** (plus `jsdom`, required by Testing Library, and `clsx` + `tailwind-merge`, required by the shadcn `cn()` helper).
|
|
69
|
+
- Configures **VS Code** (`.vscode/settings.json` and `.vscode/extensions.json`).
|
|
70
|
+
- Pins **Node.js 24** via `.nvmrc` and writes `.editorconfig`.
|
|
71
|
+
- Applies **branding**: tab title `raulmoracode` and favicon `https://cdn.raulmoracode.com/icons/favicon.ico` (in `index.html` for Vite, in the root layout metadata for Next.js).
|
|
72
|
+
- Ships a minimal **starter** for React + Vite: `public/` and `src/assets/` are emptied, `App.tsx` renders a simple hello and `App.css` starts empty.
|
|
73
|
+
- Ships a minimal **starter** for Next.js: `public/` is emptied and `page.tsx` renders a simple hello (its orphaned `page.module.css` is removed). `AGENTS.md` is kept and `CLAUDE.md` is removed.
|
|
74
|
+
- Fills in **project metadata** in `package.json`: `author` (Raul Mora, https://raulmoracode.com), `homepage` and `repository` with the GitHub URL you enter at the start.
|
|
75
|
+
- Writes **`pnpm-workspace.yaml`** with `minimumReleaseAge: 10080` plus `minimumReleaseAgeExclude` entries for every locked package, so installs keep working (pnpm enforces the policy against the whole lockfile, not just direct dependencies).
|
|
76
|
+
- Initializes Git on `main`, adds the remote, creates the commit `chore: initial project setup` and pushes with `git push -u origin main` (never `--force`). If the remote already contains commits that do not exist locally, the process stops with a clear message instead of overwriting anything.
|
|
77
|
+
|
|
78
|
+
## Generated projects
|
|
79
|
+
|
|
80
|
+
Every generated project includes:
|
|
81
|
+
|
|
82
|
+
- React + Vite or Next.js (official structure)
|
|
83
|
+
- Tailwind CSS 4
|
|
84
|
+
- shadcn ready (`components.json` with the `@raulmoracode` registry and the `cn()` helper)
|
|
85
|
+
- `@raulmoracode` registry in `.npmrc` (`${GH_TOKEN}`, only with registry access)
|
|
86
|
+
- Zustand
|
|
87
|
+
- React Hook Form + Zod
|
|
88
|
+
- TanStack Query (provider included)
|
|
89
|
+
- Biome (format, lint, organize imports)
|
|
90
|
+
- Vitest + Testing Library
|
|
91
|
+
- VS Code settings
|
|
92
|
+
- Branding (`raulmoracode` tab title and CDN favicon)
|
|
93
|
+
- `pnpm-workspace.yaml` with `minimumReleaseAge: 10080`
|
|
94
|
+
- Node.js 24 (`.nvmrc`)
|
|
95
|
+
- pnpm (`pnpm-lock.yaml`, no `package-lock.json` or `yarn.lock`)
|
|
96
|
+
- Git initialized and pushed to GitHub
|
|
97
|
+
|
|
98
|
+
### Commands in a generated project
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
pnpm install
|
|
102
|
+
pnpm dev
|
|
103
|
+
pnpm build
|
|
104
|
+
pnpm test
|
|
105
|
+
pnpm check
|
|
106
|
+
pnpm format
|
|
107
|
+
pnpm lint
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Add shadcn components (including the private registry):
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pnpm dlx shadcn@latest add card
|
|
114
|
+
pnpm dlx shadcn@latest add @raulmoracode/<component>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
After adding components, normalize their style with Biome (the shadcn CLI uses its own formatting):
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pnpm exec biome check --write .
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Vite projects resolve the `@/*` alias to `src/*` (`tsconfig.app.json`, root `tsconfig.json` and `vite.config.ts`).
|
|
124
|
+
|
|
125
|
+
Every generated project ships an `AGENTS.md` file with guidelines for AI coding agents (source of truth, conventions, validation, Git safety and security rules), plus a project tooling section (pnpm exclusively, script map and the shadcn workflow).
|
|
126
|
+
|
|
127
|
+
## GitHub authentication
|
|
128
|
+
|
|
129
|
+
The CLI runs `git push -u origin main`, so Git must be authenticated against GitHub. Any of these work:
|
|
130
|
+
|
|
131
|
+
- [GitHub CLI](https://cli.github.com): `gh auth login`
|
|
132
|
+
- A credential manager / SSH key configured for `github.com`
|
|
133
|
+
- A `GH_TOKEN` / `GITHUB_TOKEN` environment variable together with a credential helper
|
|
134
|
+
|
|
135
|
+
### Note on `GH_TOKEN` and pnpm >= 11.5.3
|
|
136
|
+
|
|
137
|
+
The generated `.npmrc` references `${GH_TOKEN}` as mandated. However, since **pnpm 11.5.3** environment variables are **not** expanded in project-level `.npmrc` credential values (a security fix for [GHSA-3qhv-2rgh-x77r](https://github.com/pnpm/security/advisories/GHSA-3qhv-2rgh-x77r)): pnpm prints a warning and ignores that line, while the registry mapping (`@raulmoracode:registry=...`) still applies. To authenticate installs from GitHub Packages, keep your token in a **user-level** location where expansion works:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
pnpm config set //npm.pkg.github.com/:_authToken "$GH_TOKEN"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
or in `~/.npmrc`:
|
|
144
|
+
|
|
145
|
+
```ini
|
|
146
|
+
//npm.pkg.github.com/:_authToken=${GH_TOKEN}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## npm
|
|
150
|
+
|
|
151
|
+
### Install
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
npm install -g @raulmoracode/create
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Update the global installation
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
npm install -g @raulmoracode/create
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Publish (maintainers)
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
pnpm install
|
|
167
|
+
pnpm build
|
|
168
|
+
pnpm test
|
|
169
|
+
npm publish --access public
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Create the `NPM_TOKEN` (granular, read+write on `@raulmoracode/create`) at `npmjs.com > Access Tokens` and save it as `Settings > Secrets > Actions > NPM_TOKEN`.
|
|
173
|
+
|
|
174
|
+
The repository also ships a GitHub Actions workflow (`.github/workflows/publish.yml`) that builds, tests and publishes the package to npmjs on every `v*` tag using `NPM_TOKEN`.
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pnpm install
|
|
180
|
+
pnpm build
|
|
181
|
+
pnpm test
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Other useful scripts:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
pnpm check # biome check .
|
|
188
|
+
pnpm format # biome format --write .
|
|
189
|
+
pnpm lint # biome lint .
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Architecture
|
|
193
|
+
|
|
194
|
+
```text
|
|
195
|
+
src/
|
|
196
|
+
├── index.ts # entry point (shebang)
|
|
197
|
+
├── cli/ # orchestration and Clack output
|
|
198
|
+
├── prompts/ # user interaction (Clack)
|
|
199
|
+
├── generators/ # project configuration steps
|
|
200
|
+
├── frameworks/ # vite / next specific logic
|
|
201
|
+
├── git/ # git command builders and wrappers
|
|
202
|
+
├── config/ # configuration file templates
|
|
203
|
+
└── utils/ # exec, filesystem, validation
|
|
204
|
+
tests/ # vitest suite (external commands are mocked)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
External commands are always executed with argument arrays through `utils/exec.ts` (never string interpolation), and every user input is validated before use.
|
|
208
|
+
|
|
209
|
+
### Verified adjustments
|
|
210
|
+
|
|
211
|
+
The following deviations from the naive expected configuration were verified against the real tools and are intentional:
|
|
212
|
+
|
|
213
|
+
- **`create-vite@9.2.1`** is used for Vite projects because `create-vite@8.3.1` does not exist; the 9.x line is the one that scaffolds the Vite 8.x line. `vite` is then pinned to `8.3.1` exactly.
|
|
214
|
+
- **`components.json`** must contain the full shadcn configuration (`$schema`, `style`, `tailwind`, `rsc`, `tsx`, `aliases`) because shadcn 4.x rejects a config with only `registries` — and `pnpm dlx shadcn@latest add card` is a hard requirement. The `@raulmoracode` registry entry is exactly the mandated one.
|
|
215
|
+
- **`biome.json`** enables organize imports through `assist.actions.source.organizeImports` because Biome 2.5.14 removed the top-level `organizeImports` key; it ignores `dist`/`.next` build output; and disables `noSvgWithoutTitle`/`noAmbiguousAnchorText`, which fire on the official Vite/Next template demo assets.
|
|
216
|
+
- **`jsdom`** (Testing Library DOM environment) and **`clsx` + `tailwind-merge`** (shadcn `cn()` helper) are installed because they are strictly required by the mandated Testing Library and shadcn setups.
|
|
217
|
+
|
|
218
|
+
## Packaging and local verification
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
pnpm build
|
|
222
|
+
npm pack
|
|
223
|
+
npm install -g ./raulmoracode-create-1.0.0.tgz
|
|
224
|
+
raulmoracode-create
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The packaged tarball contains `dist/` (including the `raulmoracode-create` bin with its shebang), `README.md` and `LICENSE`.
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
[MIT](LICENSE)
|
package/dist/cli/args.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const VERSION = "1.0.0";
|
|
2
|
+
export function parseArgs(argv) {
|
|
3
|
+
return {
|
|
4
|
+
help: argv.includes("--help") || argv.includes("-h"),
|
|
5
|
+
version: argv.includes("--version") || argv.includes("-V"),
|
|
6
|
+
verbose: argv.includes("--verbose"),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function helpText() {
|
|
10
|
+
return [
|
|
11
|
+
`raulmoracode-create v${VERSION} — scaffolds React + Vite / Next.js projects.`,
|
|
12
|
+
"",
|
|
13
|
+
"Usage:",
|
|
14
|
+
" raulmoracode-create [options]",
|
|
15
|
+
"",
|
|
16
|
+
"Options:",
|
|
17
|
+
" --verbose Print the output of every external command.",
|
|
18
|
+
" -h, --help Show this help message.",
|
|
19
|
+
" -V, --version Show the installed version.",
|
|
20
|
+
"",
|
|
21
|
+
"Environment:",
|
|
22
|
+
" GH_TOKEN (optional) Enables the private @raulmoracode/icons package.",
|
|
23
|
+
" Without registry access the project is generated",
|
|
24
|
+
" without .npmrc and without icons.",
|
|
25
|
+
"",
|
|
26
|
+
"Examples:",
|
|
27
|
+
" raulmoracode-create",
|
|
28
|
+
" raulmoracode-create --verbose",
|
|
29
|
+
"",
|
|
30
|
+
].join("\n");
|
|
31
|
+
}
|
|
32
|
+
export function printHelp() {
|
|
33
|
+
console.log(helpText());
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { intro, log, outro } from "@clack/prompts";
|
|
2
|
+
export function showIntro() {
|
|
3
|
+
intro("Raulmoracode Create");
|
|
4
|
+
}
|
|
5
|
+
export function showError(message) {
|
|
6
|
+
log.error(message);
|
|
7
|
+
}
|
|
8
|
+
export function showWarning(message) {
|
|
9
|
+
log.warn(message);
|
|
10
|
+
}
|
|
11
|
+
export function showSummary(params) {
|
|
12
|
+
const { projectName, githubUrl, frameworkLabel } = params;
|
|
13
|
+
log.success("Project created successfully!");
|
|
14
|
+
log.message(`Framework: ${frameworkLabel}`);
|
|
15
|
+
log.message(`Local: ./${projectName}`);
|
|
16
|
+
log.message(`GitHub: ${githubUrl}`);
|
|
17
|
+
}
|
|
18
|
+
export function showFarewell() {
|
|
19
|
+
outro("Proyecto creado correctamente.\n¡Hasta pronto!");
|
|
20
|
+
}
|
package/dist/cli/run.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { intro, log, tasks } from "@clack/prompts";
|
|
2
|
+
import { applyPrivateAccess } from "../config/tech.js";
|
|
3
|
+
import { getFramework } from "../frameworks/index.js";
|
|
4
|
+
import { configureBiome, formatProject, } from "../generators/configure-biome.js";
|
|
5
|
+
import { configureGitHooks } from "../generators/configure-git-hooks.js";
|
|
6
|
+
import { augmentGitignore, configureEditorconfig, configureNode, } from "../generators/configure-node.js";
|
|
7
|
+
import { installDependencies, normalizePackageJson, patchPackageJson, refreshPnpmWorkspaceExcludes, } from "../generators/configure-project.js";
|
|
8
|
+
import { configureRegistry } from "../generators/configure-registry.js";
|
|
9
|
+
import { configureShadcn } from "../generators/configure-shadcn.js";
|
|
10
|
+
import { configureTesting } from "../generators/configure-testing.js";
|
|
11
|
+
import { configureVscode } from "../generators/configure-vscode.js";
|
|
12
|
+
import { createProject } from "../generators/create-project.js";
|
|
13
|
+
import { createCommit } from "../git/commit.js";
|
|
14
|
+
import { initRepository } from "../git/init.js";
|
|
15
|
+
import { push, remoteHasDivergentCommits } from "../git/push.js";
|
|
16
|
+
import { addRemote, lsRemote } from "../git/remote.js";
|
|
17
|
+
import { promptOpenInVscode } from "../prompts/confirm.js";
|
|
18
|
+
import { promptFramework } from "../prompts/framework.js";
|
|
19
|
+
import { promptGitHubUrl } from "../prompts/github.js";
|
|
20
|
+
import { promptProjectName } from "../prompts/project.js";
|
|
21
|
+
import { promptTechPreset } from "../prompts/tech.js";
|
|
22
|
+
import { ExecError, exec } from "../utils/exec.js";
|
|
23
|
+
import { ensureDir, isDirectoryEmpty, pathExists, removeEmptyDir, removeIfExists, resolvePath, } from "../utils/filesystem.js";
|
|
24
|
+
import { checkPrivateRegistryAccess } from "../utils/registry.js";
|
|
25
|
+
import { satisfiesNodeVersion } from "../utils/validation.js";
|
|
26
|
+
import { showFarewell, showSummary, showWarning } from "./output.js";
|
|
27
|
+
const MINIMUM_NODE_MAJOR = 24;
|
|
28
|
+
export const REMOTE_CONFLICT_MESSAGE = [
|
|
29
|
+
"El repositorio remoto ya contiene cambios que no existen localmente.",
|
|
30
|
+
"",
|
|
31
|
+
"No se realizará ningún push destructivo.",
|
|
32
|
+
"Por favor, revisa el repositorio y vuelve a ejecutar el proceso.",
|
|
33
|
+
].join("\n");
|
|
34
|
+
export class PreflightError extends Error {
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Decides whether the generated directory must be removed after a failure.
|
|
38
|
+
*
|
|
39
|
+
* Past `checkDestination`, everything under `./<name>` was created by this run
|
|
40
|
+
* (the destination either did not exist or was an empty dir we removed), so it
|
|
41
|
+
* is safe to delete. Two exceptions:
|
|
42
|
+
* - `null` (failure before any task ran): nothing was created, nothing to clean.
|
|
43
|
+
* - `"Pushing to GitHub"`: the project is complete locally, only the push
|
|
44
|
+
* failed — deleting it would destroy a perfectly good project.
|
|
45
|
+
*/
|
|
46
|
+
export function shouldRemoveProjectDir(failedStep) {
|
|
47
|
+
if (failedStep === null) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (failedStep === "Pushing to GitHub") {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
async function requireCommand(command, instructions, verbose) {
|
|
56
|
+
try {
|
|
57
|
+
await exec(command, ["--version"], { verbose });
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof ExecError && error.spawnError) {
|
|
61
|
+
throw new PreflightError(`${command} no está instalado o no disponible en el PATH.\n${instructions}`);
|
|
62
|
+
}
|
|
63
|
+
throw new PreflightError(`No se pudo ejecutar "${command} --version".`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function requireGitIdentity(verbose) {
|
|
67
|
+
const name = await exec("git", ["config", "--get", "user.name"], { verbose });
|
|
68
|
+
const email = await exec("git", ["config", "--get", "user.email"], {
|
|
69
|
+
verbose,
|
|
70
|
+
});
|
|
71
|
+
if (!name.stdout.trim() || !email.stdout.trim()) {
|
|
72
|
+
throw new PreflightError("Git no tiene configurada la identidad (user.name y user.email), necesaria para crear el commit inicial.\n" +
|
|
73
|
+
'Ejecuta: git config --global user.name "Tu Nombre" && git config --global user.email "tu@email.com"');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function preflightChecks(verbose) {
|
|
77
|
+
const nodeCheck = satisfiesNodeVersion(process.version, MINIMUM_NODE_MAJOR);
|
|
78
|
+
if (!nodeCheck.valid) {
|
|
79
|
+
throw new PreflightError(`${nodeCheck.error}\nInstala Node.js ${MINIMUM_NODE_MAJOR} LTS: https://nodejs.org`);
|
|
80
|
+
}
|
|
81
|
+
await requireCommand("pnpm", "Instala pnpm: https://pnpm.io/installation", verbose);
|
|
82
|
+
await requireCommand("git", "Instala Git: https://git-scm.com/downloads", verbose);
|
|
83
|
+
await requireGitIdentity(verbose);
|
|
84
|
+
}
|
|
85
|
+
async function checkDestination(projectName) {
|
|
86
|
+
const projectDir = resolvePath(process.cwd(), projectName);
|
|
87
|
+
if (await pathExists(projectDir)) {
|
|
88
|
+
if (await isDirectoryEmpty(projectDir)) {
|
|
89
|
+
await removeEmptyDir(projectDir);
|
|
90
|
+
return { projectDir, reusedEmptyDir: true };
|
|
91
|
+
}
|
|
92
|
+
throw new PreflightError(`El directorio "./${projectName}" ya existe y no está vacío.\n` +
|
|
93
|
+
"Muévalo, renómbralo o elimínalo antes de continuar. No se borrará ningún contenido existente.");
|
|
94
|
+
}
|
|
95
|
+
return { projectDir, reusedEmptyDir: false };
|
|
96
|
+
}
|
|
97
|
+
async function checkRemoteAccess(githubUrl, verbose) {
|
|
98
|
+
try {
|
|
99
|
+
await lsRemote(githubUrl, verbose);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
throw new PreflightError(`No se pudo acceder al repositorio ${githubUrl}.\n` +
|
|
103
|
+
"Comprueba la URL, tu conexión a internet y tu autenticación de GitHub (https o gh auth).");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export async function run(options = {}) {
|
|
107
|
+
const verbose = options.verbose ?? false;
|
|
108
|
+
intro("Raulmoracode Create");
|
|
109
|
+
let projectName = "";
|
|
110
|
+
let cleanupDir = null;
|
|
111
|
+
let restoreEmptyDir = false;
|
|
112
|
+
let failedStep = null;
|
|
113
|
+
try {
|
|
114
|
+
await preflightChecks(verbose);
|
|
115
|
+
const frameworkId = await promptFramework();
|
|
116
|
+
const { selection, notes } = await promptTechPreset();
|
|
117
|
+
for (const note of notes) {
|
|
118
|
+
showWarning(note);
|
|
119
|
+
}
|
|
120
|
+
projectName = await promptProjectName();
|
|
121
|
+
const githubUrl = await promptGitHubUrl();
|
|
122
|
+
const { projectDir, reusedEmptyDir } = await checkDestination(projectName);
|
|
123
|
+
cleanupDir = projectDir;
|
|
124
|
+
restoreEmptyDir = reusedEmptyDir;
|
|
125
|
+
await checkRemoteAccess(githubUrl, verbose);
|
|
126
|
+
const hasPrivateAccess = await checkPrivateRegistryAccess();
|
|
127
|
+
const effectiveSelection = applyPrivateAccess(selection, hasPrivateAccess);
|
|
128
|
+
if (selection.registry && !hasPrivateAccess) {
|
|
129
|
+
showWarning("Sin acceso al registro privado @raulmoracode (GH_TOKEN ausente o sin permisos): " +
|
|
130
|
+
"se omite @raulmoracode/icons y el .npmrc.");
|
|
131
|
+
}
|
|
132
|
+
const framework = getFramework(frameworkId);
|
|
133
|
+
await tasks([
|
|
134
|
+
{
|
|
135
|
+
title: "Creating project",
|
|
136
|
+
task: async (message) => {
|
|
137
|
+
failedStep = "Creating project";
|
|
138
|
+
message(`Scaffolding with ${framework.label}`);
|
|
139
|
+
await createProject(framework, projectName, process.cwd(), verbose);
|
|
140
|
+
if (effectiveSelection.tailwind) {
|
|
141
|
+
message("Configuring Tailwind CSS");
|
|
142
|
+
await framework.configureTailwind(projectDir);
|
|
143
|
+
}
|
|
144
|
+
message("Configuring branding");
|
|
145
|
+
await framework.configureBranding(projectDir);
|
|
146
|
+
if (effectiveSelection.shadcn) {
|
|
147
|
+
message("Configuring shadcn");
|
|
148
|
+
await configureShadcn(projectDir, framework);
|
|
149
|
+
}
|
|
150
|
+
if (effectiveSelection.registry) {
|
|
151
|
+
message("Configuring @raulmoracode registry");
|
|
152
|
+
await configureRegistry(projectDir);
|
|
153
|
+
}
|
|
154
|
+
if (effectiveSelection["tanstack-query"]) {
|
|
155
|
+
message("Configuring TanStack Query");
|
|
156
|
+
await framework.configureTanStackQuery(projectDir);
|
|
157
|
+
}
|
|
158
|
+
if (framework.configureStarter) {
|
|
159
|
+
message("Configuring starter");
|
|
160
|
+
await framework.configureStarter(projectDir);
|
|
161
|
+
}
|
|
162
|
+
if (effectiveSelection.biome) {
|
|
163
|
+
message("Configuring Biome");
|
|
164
|
+
await configureBiome(projectDir);
|
|
165
|
+
}
|
|
166
|
+
if (effectiveSelection.testing) {
|
|
167
|
+
message("Configuring testing");
|
|
168
|
+
await configureTesting(projectDir);
|
|
169
|
+
}
|
|
170
|
+
if (effectiveSelection.vscode) {
|
|
171
|
+
message("Configuring VS Code");
|
|
172
|
+
await configureVscode(projectDir);
|
|
173
|
+
}
|
|
174
|
+
message("Configuring Node version");
|
|
175
|
+
await configureNode(projectDir);
|
|
176
|
+
await configureEditorconfig(projectDir);
|
|
177
|
+
if (effectiveSelection.husky) {
|
|
178
|
+
message("Configuring Git hooks");
|
|
179
|
+
await configureGitHooks(projectDir, effectiveSelection);
|
|
180
|
+
}
|
|
181
|
+
await patchPackageJson(projectDir, framework, projectName, githubUrl, effectiveSelection);
|
|
182
|
+
await augmentGitignore(projectDir);
|
|
183
|
+
return "Project created";
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
title: "Installing dependencies",
|
|
188
|
+
task: async (message) => {
|
|
189
|
+
failedStep = "Installing dependencies";
|
|
190
|
+
message("Installing base and additional dependencies");
|
|
191
|
+
await installDependencies(projectDir, framework, verbose, effectiveSelection);
|
|
192
|
+
await normalizePackageJson(projectDir);
|
|
193
|
+
if (effectiveSelection.biome) {
|
|
194
|
+
message("Formatting project with Biome");
|
|
195
|
+
await formatProject(projectDir, verbose);
|
|
196
|
+
}
|
|
197
|
+
message("Configuring pnpm workspace");
|
|
198
|
+
await refreshPnpmWorkspaceExcludes(projectDir, framework, verbose, effectiveSelection);
|
|
199
|
+
if (effectiveSelection.zustand) {
|
|
200
|
+
message("Zustand configured");
|
|
201
|
+
}
|
|
202
|
+
if (effectiveSelection.forms) {
|
|
203
|
+
message("React Hook Form + Zod configured");
|
|
204
|
+
}
|
|
205
|
+
if (effectiveSelection["tanstack-query"]) {
|
|
206
|
+
message("TanStack Query configured");
|
|
207
|
+
}
|
|
208
|
+
if (effectiveSelection.tailwind) {
|
|
209
|
+
message("Tailwind configured");
|
|
210
|
+
}
|
|
211
|
+
if (effectiveSelection.biome) {
|
|
212
|
+
message("Biome configured");
|
|
213
|
+
}
|
|
214
|
+
if (effectiveSelection.testing) {
|
|
215
|
+
message("Testing configured");
|
|
216
|
+
}
|
|
217
|
+
return "Dependencies installed";
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
title: "Initializing Git",
|
|
222
|
+
task: async () => {
|
|
223
|
+
failedStep = "Initializing Git";
|
|
224
|
+
await initRepository(projectDir, verbose);
|
|
225
|
+
if (effectiveSelection.husky) {
|
|
226
|
+
await exec("pnpm", ["exec", "husky"], {
|
|
227
|
+
cwd: projectDir,
|
|
228
|
+
verbose,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return "Git initialized";
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
title: "Configuring remote",
|
|
236
|
+
task: async () => {
|
|
237
|
+
failedStep = "Configuring remote";
|
|
238
|
+
await addRemote(projectDir, githubUrl, verbose);
|
|
239
|
+
return "Remote configured";
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
title: "Creating initial commit",
|
|
244
|
+
task: async () => {
|
|
245
|
+
failedStep = "Creating initial commit";
|
|
246
|
+
await createCommit(projectDir, verbose);
|
|
247
|
+
return "Initial commit created";
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
title: "Pushing to GitHub",
|
|
252
|
+
task: async () => {
|
|
253
|
+
failedStep = "Pushing to GitHub";
|
|
254
|
+
const hasConflicts = await remoteHasDivergentCommits(projectDir, verbose);
|
|
255
|
+
if (hasConflicts) {
|
|
256
|
+
throw new Error(REMOTE_CONFLICT_MESSAGE);
|
|
257
|
+
}
|
|
258
|
+
await push(projectDir, verbose);
|
|
259
|
+
return "Pushed to GitHub";
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
]);
|
|
263
|
+
showSummary({ projectName, githubUrl, frameworkLabel: framework.label });
|
|
264
|
+
const openInVscode = await promptOpenInVscode();
|
|
265
|
+
if (openInVscode) {
|
|
266
|
+
try {
|
|
267
|
+
await exec("code", ["."], { cwd: projectDir, verbose });
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (error instanceof ExecError && error.spawnError) {
|
|
271
|
+
showWarning('El comando "code" no está disponible. Abre el proyecto manualmente con VS Code.');
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
showWarning("No se pudo abrir VS Code automáticamente. Abre el proyecto manualmente.");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
showFarewell();
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
const cancelled = error instanceof ExecError &&
|
|
282
|
+
(error.signal === "SIGINT" || error.signal === "SIGTERM");
|
|
283
|
+
let cleaned = false;
|
|
284
|
+
if (shouldRemoveProjectDir(failedStep) && cleanupDir !== null) {
|
|
285
|
+
try {
|
|
286
|
+
await removeIfExists(cleanupDir);
|
|
287
|
+
cleaned = true;
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
cleaned = false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else if (failedStep === null && restoreEmptyDir && cleanupDir !== null) {
|
|
294
|
+
try {
|
|
295
|
+
await ensureDir(cleanupDir);
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// Best effort: restoring an empty dir must never hide the real error.
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (cancelled) {
|
|
302
|
+
if (cleaned) {
|
|
303
|
+
log.error(`Operación cancelada. Se eliminó "./${projectName}" (todo su contenido lo había creado el CLI).`);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
log.error("Operación cancelada por el usuario.");
|
|
307
|
+
}
|
|
308
|
+
process.exit(130);
|
|
309
|
+
}
|
|
310
|
+
if (error instanceof Error) {
|
|
311
|
+
log.error(error.message);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
log.error(String(error));
|
|
315
|
+
}
|
|
316
|
+
if (cleaned) {
|
|
317
|
+
showWarning(`Se eliminó "./${projectName}" (todo su contenido lo había creado el CLI). ` +
|
|
318
|
+
"Vuelve a ejecutar raulmoracode-create para reintentar.");
|
|
319
|
+
}
|
|
320
|
+
else if (failedStep === "Pushing to GitHub") {
|
|
321
|
+
showWarning(`El proyecto está completo en "./${projectName}": solo falló el push. ` +
|
|
322
|
+
`Reintenta con: git -C "./${projectName}" push -u origin main`);
|
|
323
|
+
}
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
}
|