@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/dist/git/push.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { exec } from "../utils/exec.js";
|
|
2
|
+
export function pushArgs() {
|
|
3
|
+
return ["push", "-u", "origin", "main"];
|
|
4
|
+
}
|
|
5
|
+
export function fetchArgs() {
|
|
6
|
+
return ["fetch", "origin"];
|
|
7
|
+
}
|
|
8
|
+
export function revParseArgs(ref) {
|
|
9
|
+
return ["rev-parse", ref];
|
|
10
|
+
}
|
|
11
|
+
export function mergeBaseIsAncestorArgs(ancestor, descendant) {
|
|
12
|
+
return ["merge-base", "--is-ancestor", ancestor, descendant];
|
|
13
|
+
}
|
|
14
|
+
export async function push(projectDir, verbose) {
|
|
15
|
+
await exec("git", pushArgs(), { cwd: projectDir, verbose });
|
|
16
|
+
}
|
|
17
|
+
export async function fetchRemote(projectDir, verbose) {
|
|
18
|
+
await exec("git", fetchArgs(), { cwd: projectDir, verbose });
|
|
19
|
+
}
|
|
20
|
+
export async function resolveSha(projectDir, ref, verbose) {
|
|
21
|
+
try {
|
|
22
|
+
const result = await exec("git", revParseArgs(ref), {
|
|
23
|
+
cwd: projectDir,
|
|
24
|
+
verbose,
|
|
25
|
+
});
|
|
26
|
+
const sha = result.stdout.trim();
|
|
27
|
+
return sha.length > 0 ? sha : null;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function isAncestor(projectDir, ancestor, descendant, verbose) {
|
|
34
|
+
try {
|
|
35
|
+
await exec("git", mergeBaseIsAncestorArgs(ancestor, descendant), {
|
|
36
|
+
cwd: projectDir,
|
|
37
|
+
verbose,
|
|
38
|
+
});
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function remoteHasDivergentCommits(projectDir, verbose) {
|
|
46
|
+
await fetchRemote(projectDir, verbose);
|
|
47
|
+
const remoteSha = await resolveSha(projectDir, "origin/main", verbose);
|
|
48
|
+
if (!remoteSha) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const localSha = await resolveSha(projectDir, "HEAD", verbose);
|
|
52
|
+
if (!localSha) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (remoteSha === localSha) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return !(await isAncestor(projectDir, remoteSha, "HEAD", verbose));
|
|
59
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { exec } from "../utils/exec.js";
|
|
2
|
+
export function remoteAddArgs(url) {
|
|
3
|
+
return ["remote", "add", "origin", url];
|
|
4
|
+
}
|
|
5
|
+
export function remoteGetUrlArgs() {
|
|
6
|
+
return ["remote", "get-url", "origin"];
|
|
7
|
+
}
|
|
8
|
+
export function lsRemoteArgs(url) {
|
|
9
|
+
return ["ls-remote", url];
|
|
10
|
+
}
|
|
11
|
+
export async function addRemote(projectDir, url, verbose) {
|
|
12
|
+
await exec("git", remoteAddArgs(url), { cwd: projectDir, verbose });
|
|
13
|
+
}
|
|
14
|
+
export async function remoteExists(projectDir, verbose) {
|
|
15
|
+
try {
|
|
16
|
+
await exec("git", remoteGetUrlArgs(), { cwd: projectDir, verbose });
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function lsRemote(url, verbose) {
|
|
24
|
+
const result = await exec("git", lsRemoteArgs(url), { verbose });
|
|
25
|
+
return result.stdout;
|
|
26
|
+
}
|
|
27
|
+
export async function isRemoteEmpty(url, verbose) {
|
|
28
|
+
const output = await lsRemote(url, verbose);
|
|
29
|
+
return output.trim() === "";
|
|
30
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs, printHelp, VERSION } from "./cli/args.js";
|
|
3
|
+
import { run } from "./cli/run.js";
|
|
4
|
+
const args = parseArgs(process.argv.slice(2));
|
|
5
|
+
if (args.help) {
|
|
6
|
+
printHelp();
|
|
7
|
+
process.exit(0);
|
|
8
|
+
}
|
|
9
|
+
if (args.version) {
|
|
10
|
+
console.log(VERSION);
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
run({ verbose: args.verbose }).catch((error) => {
|
|
14
|
+
console.error(error);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { confirm } from "@clack/prompts";
|
|
2
|
+
import { ensureNotCancelled } from "./cancel.js";
|
|
3
|
+
export async function promptOpenInVscode() {
|
|
4
|
+
const answer = ensureNotCancelled(await confirm({
|
|
5
|
+
message: `¿Quieres abrir el proyecto ahora?`,
|
|
6
|
+
active: "Sí",
|
|
7
|
+
inactive: "No",
|
|
8
|
+
initialValue: true,
|
|
9
|
+
}));
|
|
10
|
+
return Boolean(answer);
|
|
11
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { select } from "@clack/prompts";
|
|
2
|
+
import { isFramework } from "../utils/validation.js";
|
|
3
|
+
import { ensureNotCancelled } from "./cancel.js";
|
|
4
|
+
export async function promptFramework() {
|
|
5
|
+
const framework = ensureNotCancelled(await select({
|
|
6
|
+
message: "¿Qué tecnología quieres utilizar?",
|
|
7
|
+
options: [
|
|
8
|
+
{
|
|
9
|
+
value: "vite",
|
|
10
|
+
label: "React + Vite",
|
|
11
|
+
hint: "Vite 8, React 19 y TypeScript 7",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
value: "next",
|
|
15
|
+
label: "Next.js",
|
|
16
|
+
hint: "Next.js 16, App Router y React 19",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
initialValue: "vite",
|
|
20
|
+
}));
|
|
21
|
+
if (!isFramework(framework)) {
|
|
22
|
+
throw new Error(`Framework desconocido: ${String(framework)}`);
|
|
23
|
+
}
|
|
24
|
+
return framework;
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { text } from "@clack/prompts";
|
|
2
|
+
import { validateGitHubUrl } from "../utils/validation.js";
|
|
3
|
+
import { ensureNotCancelled } from "./cancel.js";
|
|
4
|
+
export async function promptGitHubUrl() {
|
|
5
|
+
const url = ensureNotCancelled(await text({
|
|
6
|
+
message: "¿Dónde se va a alojar el código?",
|
|
7
|
+
placeholder: "https://github.com/raulmoracode/my-project",
|
|
8
|
+
validate(value) {
|
|
9
|
+
if (!value) {
|
|
10
|
+
return "La URL del repositorio no puede estar vacía.";
|
|
11
|
+
}
|
|
12
|
+
const result = validateGitHubUrl(value);
|
|
13
|
+
return result.valid ? undefined : result.error;
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
const trimmed = url
|
|
17
|
+
.trim()
|
|
18
|
+
.replace(/\/+$/, "")
|
|
19
|
+
.replace(/\.git$/, "");
|
|
20
|
+
const check = validateGitHubUrl(trimmed);
|
|
21
|
+
if (!check.valid) {
|
|
22
|
+
throw new Error(check.error);
|
|
23
|
+
}
|
|
24
|
+
return trimmed;
|
|
25
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { text } from "@clack/prompts";
|
|
2
|
+
import { validateProjectName } from "../utils/validation.js";
|
|
3
|
+
import { ensureNotCancelled } from "./cancel.js";
|
|
4
|
+
export async function promptProjectName() {
|
|
5
|
+
const name = ensureNotCancelled(await text({
|
|
6
|
+
message: "¿Cuál es el nombre del proyecto?",
|
|
7
|
+
placeholder: "my-project",
|
|
8
|
+
validate(value) {
|
|
9
|
+
if (!value) {
|
|
10
|
+
return "El nombre del proyecto no puede estar vacío.";
|
|
11
|
+
}
|
|
12
|
+
const result = validateProjectName(value);
|
|
13
|
+
return result.valid ? undefined : result.error;
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
const trimmed = name.trim();
|
|
17
|
+
const check = validateProjectName(trimmed);
|
|
18
|
+
if (!check.valid) {
|
|
19
|
+
throw new Error(check.error);
|
|
20
|
+
}
|
|
21
|
+
return trimmed;
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { multiselect } from "@clack/prompts";
|
|
2
|
+
import { resolveTechSelection, TECH_IDS, TECH_OPTIONS, } from "../config/tech.js";
|
|
3
|
+
import { ensureNotCancelled } from "./cancel.js";
|
|
4
|
+
export async function promptTechPreset() {
|
|
5
|
+
const values = ensureNotCancelled(await multiselect({
|
|
6
|
+
message: "¿Qué tecnologías quieres incluir? (espacio para marcar/desmarcar)",
|
|
7
|
+
options: TECH_OPTIONS.map((option) => ({
|
|
8
|
+
value: option.id,
|
|
9
|
+
label: option.label,
|
|
10
|
+
hint: option.hint,
|
|
11
|
+
})),
|
|
12
|
+
initialValues: [...TECH_IDS],
|
|
13
|
+
required: false,
|
|
14
|
+
}));
|
|
15
|
+
const selected = new Set(values);
|
|
16
|
+
const input = Object.fromEntries(TECH_IDS.map((id) => [id, selected.has(id)]));
|
|
17
|
+
return resolveTechSelection(input);
|
|
18
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export class ExecError extends Error {
|
|
3
|
+
command;
|
|
4
|
+
args;
|
|
5
|
+
code;
|
|
6
|
+
signal;
|
|
7
|
+
stdout;
|
|
8
|
+
stderr;
|
|
9
|
+
spawnError;
|
|
10
|
+
constructor(params) {
|
|
11
|
+
const label = `${params.command} ${params.args.join(" ")}`.trim();
|
|
12
|
+
const detail = params.spawnError
|
|
13
|
+
? `Failed to spawn "${params.command}": ${params.spawnError}`
|
|
14
|
+
: params.stderr.trim() ||
|
|
15
|
+
params.stdout.trim() ||
|
|
16
|
+
`Exited with code ${params.code}`;
|
|
17
|
+
super(`Command failed: ${label}\n${detail}`);
|
|
18
|
+
this.name = "ExecError";
|
|
19
|
+
this.command = params.command;
|
|
20
|
+
this.args = params.args;
|
|
21
|
+
this.code = params.code;
|
|
22
|
+
this.signal = params.signal;
|
|
23
|
+
this.stdout = params.stdout;
|
|
24
|
+
this.stderr = params.stderr;
|
|
25
|
+
this.spawnError = params.spawnError ?? null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function resolveExecutable(name) {
|
|
29
|
+
if (process.platform !== "win32")
|
|
30
|
+
return name;
|
|
31
|
+
if (/[\\/]/.test(name))
|
|
32
|
+
return name;
|
|
33
|
+
if (name.endsWith(".cmd") || name.endsWith(".exe") || name.endsWith(".bat"))
|
|
34
|
+
return name;
|
|
35
|
+
return `${name}.cmd`;
|
|
36
|
+
}
|
|
37
|
+
export function resolveCommand(command) {
|
|
38
|
+
return resolveExecutable(command);
|
|
39
|
+
}
|
|
40
|
+
export function formatCommand(command, args) {
|
|
41
|
+
return [command, ...args].join(" ");
|
|
42
|
+
}
|
|
43
|
+
export async function exec(command, args, options = {}) {
|
|
44
|
+
const resolved = resolveExecutable(command);
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
let child;
|
|
47
|
+
try {
|
|
48
|
+
child = spawn(resolved, args, {
|
|
49
|
+
cwd: options.cwd,
|
|
50
|
+
env: options.env ?? process.env,
|
|
51
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
reject(new ExecError({
|
|
56
|
+
command,
|
|
57
|
+
args,
|
|
58
|
+
code: null,
|
|
59
|
+
signal: null,
|
|
60
|
+
stdout: "",
|
|
61
|
+
stderr: "",
|
|
62
|
+
spawnError: error instanceof Error ? error.message : String(error),
|
|
63
|
+
}));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
let stdout = "";
|
|
67
|
+
let stderr = "";
|
|
68
|
+
let settled = false;
|
|
69
|
+
child.stdout?.on("data", (chunk) => {
|
|
70
|
+
const text = chunk.toString();
|
|
71
|
+
stdout += text;
|
|
72
|
+
if (options.verbose)
|
|
73
|
+
process.stdout.write(text);
|
|
74
|
+
});
|
|
75
|
+
child.stderr?.on("data", (chunk) => {
|
|
76
|
+
const text = chunk.toString();
|
|
77
|
+
stderr += text;
|
|
78
|
+
if (options.verbose)
|
|
79
|
+
process.stderr.write(text);
|
|
80
|
+
});
|
|
81
|
+
child.on("error", (error) => {
|
|
82
|
+
if (settled)
|
|
83
|
+
return;
|
|
84
|
+
settled = true;
|
|
85
|
+
reject(new ExecError({
|
|
86
|
+
command,
|
|
87
|
+
args,
|
|
88
|
+
code: null,
|
|
89
|
+
signal: null,
|
|
90
|
+
stdout,
|
|
91
|
+
stderr,
|
|
92
|
+
spawnError: error.message,
|
|
93
|
+
}));
|
|
94
|
+
});
|
|
95
|
+
child.on("close", (code, signal) => {
|
|
96
|
+
if (settled)
|
|
97
|
+
return;
|
|
98
|
+
settled = true;
|
|
99
|
+
if (code === 0) {
|
|
100
|
+
resolve({ code, stdout, stderr });
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
reject(new ExecError({ command, args, code, signal, stdout, stderr }));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { access, chmod, mkdir, readdir, readFile, rm, rmdir, stat, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
export async function pathExists(path) {
|
|
4
|
+
try {
|
|
5
|
+
await access(path);
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export async function isDirectory(path) {
|
|
13
|
+
try {
|
|
14
|
+
return (await stat(path)).isDirectory();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function isDirectoryEmpty(path) {
|
|
21
|
+
const entries = await readdir(path);
|
|
22
|
+
return entries.length === 0;
|
|
23
|
+
}
|
|
24
|
+
export async function listDirEntries(path) {
|
|
25
|
+
return readdir(path);
|
|
26
|
+
}
|
|
27
|
+
export async function ensureDir(path) {
|
|
28
|
+
await mkdir(path, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
export async function writeTextFile(path, content) {
|
|
31
|
+
await ensureDir(dirname(path));
|
|
32
|
+
await writeFile(path, content, "utf8");
|
|
33
|
+
}
|
|
34
|
+
export async function readTextFile(path) {
|
|
35
|
+
return readFile(path, "utf8");
|
|
36
|
+
}
|
|
37
|
+
export async function readJsonFile(path) {
|
|
38
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
39
|
+
}
|
|
40
|
+
export function stripJsonComments(content) {
|
|
41
|
+
let result = "";
|
|
42
|
+
let i = 0;
|
|
43
|
+
let inString = false;
|
|
44
|
+
while (i < content.length) {
|
|
45
|
+
const char = content[i] ?? "";
|
|
46
|
+
if (inString) {
|
|
47
|
+
result += char;
|
|
48
|
+
if (char === "\\") {
|
|
49
|
+
result += content[i + 1] ?? "";
|
|
50
|
+
i += 2;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (char === '"') {
|
|
54
|
+
inString = false;
|
|
55
|
+
}
|
|
56
|
+
i += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (char === '"') {
|
|
60
|
+
inString = true;
|
|
61
|
+
result += char;
|
|
62
|
+
i += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (char === "/" && content[i + 1] === "/") {
|
|
66
|
+
while (i < content.length && content[i] !== "\n") {
|
|
67
|
+
i += 1;
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (char === "/" && content[i + 1] === "*") {
|
|
72
|
+
i += 2;
|
|
73
|
+
while (i < content.length &&
|
|
74
|
+
!(content[i] === "*" && content[i + 1] === "/")) {
|
|
75
|
+
i += 1;
|
|
76
|
+
}
|
|
77
|
+
i += 2;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
result += char;
|
|
81
|
+
i += 1;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
export function parseJsonc(content) {
|
|
86
|
+
return JSON.parse(stripJsonComments(content));
|
|
87
|
+
}
|
|
88
|
+
export async function removeIfExists(path) {
|
|
89
|
+
await rm(path, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
export function joinPath(...parts) {
|
|
92
|
+
return join(...parts);
|
|
93
|
+
}
|
|
94
|
+
export function resolvePath(...parts) {
|
|
95
|
+
return resolve(...parts);
|
|
96
|
+
}
|
|
97
|
+
export async function removeEmptyDir(path) {
|
|
98
|
+
try {
|
|
99
|
+
await rmdir(path);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export async function makeExecutable(path) {
|
|
107
|
+
await chmod(path, 0o755);
|
|
108
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const PRIVATE_ICONS_METADATA_URL = "https://npm.pkg.github.com/@raulmoracode%2Ficons";
|
|
2
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
3
|
+
export async function checkPrivateRegistryAccess() {
|
|
4
|
+
const token = process.env.GH_TOKEN;
|
|
5
|
+
if (!token) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
const response = await fetch(PRIVATE_ICONS_METADATA_URL, {
|
|
10
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
11
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
12
|
+
});
|
|
13
|
+
return response.ok;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const MAX_PROJECT_NAME_LENGTH = 214;
|
|
2
|
+
const RESERVED_PROJECT_NAMES = new Set([
|
|
3
|
+
"node_modules",
|
|
4
|
+
"favicon.ico",
|
|
5
|
+
"con",
|
|
6
|
+
"prn",
|
|
7
|
+
"aux",
|
|
8
|
+
"nul",
|
|
9
|
+
"com1",
|
|
10
|
+
"com2",
|
|
11
|
+
"com3",
|
|
12
|
+
"com4",
|
|
13
|
+
"com5",
|
|
14
|
+
"com6",
|
|
15
|
+
"com7",
|
|
16
|
+
"com8",
|
|
17
|
+
"com9",
|
|
18
|
+
"lpt1",
|
|
19
|
+
"lpt2",
|
|
20
|
+
"lpt3",
|
|
21
|
+
"lpt4",
|
|
22
|
+
"lpt5",
|
|
23
|
+
"lpt6",
|
|
24
|
+
"lpt7",
|
|
25
|
+
"lpt8",
|
|
26
|
+
"lpt9",
|
|
27
|
+
]);
|
|
28
|
+
const PROJECT_NAME_REGEX = /^[a-z0-9][a-z0-9._~-]*$/;
|
|
29
|
+
const GITHUB_OWNER_REGEX = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,38})$/;
|
|
30
|
+
const GITHUB_REPO_REGEX = /^[a-zA-Z0-9._-]+$/;
|
|
31
|
+
export function validateProjectName(name) {
|
|
32
|
+
if (name.length === 0) {
|
|
33
|
+
return {
|
|
34
|
+
valid: false,
|
|
35
|
+
error: "El nombre del proyecto no puede estar vacío.",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (name !== name.trim()) {
|
|
39
|
+
return {
|
|
40
|
+
valid: false,
|
|
41
|
+
error: "El nombre del proyecto no puede contener espacios al inicio o al final.",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (name.length > MAX_PROJECT_NAME_LENGTH) {
|
|
45
|
+
return {
|
|
46
|
+
valid: false,
|
|
47
|
+
error: `El nombre del proyecto no puede superar ${MAX_PROJECT_NAME_LENGTH} caracteres.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (name.startsWith(".") || name.startsWith("_")) {
|
|
51
|
+
return {
|
|
52
|
+
valid: false,
|
|
53
|
+
error: "El nombre del proyecto no puede empezar por '.' o '_'.",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (!PROJECT_NAME_REGEX.test(name)) {
|
|
57
|
+
return {
|
|
58
|
+
valid: false,
|
|
59
|
+
error: "El nombre del proyecto solo puede contener minúsculas, números y los caracteres '-', '_', '.', '~'.",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (RESERVED_PROJECT_NAMES.has(name.toLowerCase())) {
|
|
63
|
+
return {
|
|
64
|
+
valid: false,
|
|
65
|
+
error: `"${name}" es un nombre reservado y no puede utilizarse.`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { valid: true };
|
|
69
|
+
}
|
|
70
|
+
export function validateGitHubUrl(url) {
|
|
71
|
+
if (url.length === 0) {
|
|
72
|
+
return {
|
|
73
|
+
valid: false,
|
|
74
|
+
error: "La URL del repositorio no puede estar vacía.",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = new URL(url);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return { valid: false, error: "La URL proporcionada no es válida." };
|
|
83
|
+
}
|
|
84
|
+
if (parsed.protocol !== "https:") {
|
|
85
|
+
return {
|
|
86
|
+
valid: false,
|
|
87
|
+
error: "La URL del repositorio debe utilizar https://.",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (parsed.hostname !== "github.com") {
|
|
91
|
+
return { valid: false, error: "La URL debe apuntar a github.com." };
|
|
92
|
+
}
|
|
93
|
+
if (parsed.username || parsed.password) {
|
|
94
|
+
return { valid: false, error: "La URL no debe incluir credenciales." };
|
|
95
|
+
}
|
|
96
|
+
const parts = parsed.pathname
|
|
97
|
+
.replace(/\/+$/, "")
|
|
98
|
+
.replace(/\.git$/, "")
|
|
99
|
+
.split("/")
|
|
100
|
+
.filter(Boolean);
|
|
101
|
+
if (parts.length !== 2) {
|
|
102
|
+
return {
|
|
103
|
+
valid: false,
|
|
104
|
+
error: "Formato esperado: https://github.com/<usuario>/<repositorio>",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const [owner, repo] = parts;
|
|
108
|
+
if (!GITHUB_OWNER_REGEX.test(owner)) {
|
|
109
|
+
return {
|
|
110
|
+
valid: false,
|
|
111
|
+
error: "El nombre de usuario de GitHub no es válido.",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (!GITHUB_REPO_REGEX.test(repo) || repo === "." || repo === "..") {
|
|
115
|
+
return { valid: false, error: "El nombre del repositorio no es válido." };
|
|
116
|
+
}
|
|
117
|
+
return { valid: true, owner, repo };
|
|
118
|
+
}
|
|
119
|
+
export function isFramework(value) {
|
|
120
|
+
return value === "vite" || value === "next";
|
|
121
|
+
}
|
|
122
|
+
export function validateFramework(value) {
|
|
123
|
+
if (isFramework(value)) {
|
|
124
|
+
return { valid: true };
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
valid: false,
|
|
128
|
+
error: `Framework desconocido: ${String(value)}. Valores válidos: "vite", "next".`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export function satisfiesNodeVersion(version, minimumMajor) {
|
|
132
|
+
const match = /^v?(\d+)/.exec(version);
|
|
133
|
+
if (!match) {
|
|
134
|
+
return {
|
|
135
|
+
valid: false,
|
|
136
|
+
error: `No se pudo interpretar la versión de Node.js: ${version}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const major = Number(match[1]);
|
|
140
|
+
if (!Number.isFinite(major)) {
|
|
141
|
+
return {
|
|
142
|
+
valid: false,
|
|
143
|
+
error: `No se pudo interpretar la versión de Node.js: ${version}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (major < minimumMajor) {
|
|
147
|
+
return {
|
|
148
|
+
valid: false,
|
|
149
|
+
error: `Se requiere Node.js ${minimumMajor} o superior. Versión actual: ${version}.`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return { valid: true };
|
|
153
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@raulmoracode/create",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Professional CLI to scaffold React + Vite and Next.js projects with Tailwind CSS, shadcn, Biome, Vitest and the @raulmoracode registry.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"raulmoracode",
|
|
7
|
+
"scaffold",
|
|
8
|
+
"scaffolder",
|
|
9
|
+
"cli",
|
|
10
|
+
"react",
|
|
11
|
+
"vite",
|
|
12
|
+
"nextjs",
|
|
13
|
+
"tailwindcss",
|
|
14
|
+
"shadcn",
|
|
15
|
+
"biome",
|
|
16
|
+
"vitest",
|
|
17
|
+
"pnpm"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://github.com/raulmoracode/raulmoracode-create#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/raulmoracode/raulmoracode-create/issues"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/raulmoracode/raulmoracode-create.git"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"author": "raulmoracode",
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": "./dist/index.js",
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"bin": {
|
|
36
|
+
"raulmoracode-create": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc -p tsconfig.json",
|
|
45
|
+
"check": "biome check .",
|
|
46
|
+
"format": "biome format --write .",
|
|
47
|
+
"lint": "biome lint .",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"prepublishOnly": "pnpm build"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@clack/prompts": "1.8.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@biomejs/biome": "2.5.14",
|
|
56
|
+
"@types/node": "24.19.0",
|
|
57
|
+
"typescript": "7.0.2",
|
|
58
|
+
"vitest": "5.0.2"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=24"
|
|
62
|
+
},
|
|
63
|
+
"packageManager": "pnpm@12.6.0",
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"registry": "https://registry.npmjs.org/",
|
|
66
|
+
"access": "public"
|
|
67
|
+
}
|
|
68
|
+
}
|