create-ropo 0.1.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 +45 -0
- package/index.js +191 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roberto Rodriguez Carbonell
|
|
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,45 @@
|
|
|
1
|
+
# create-ropo
|
|
2
|
+
|
|
3
|
+
Scaffold a new project from the [`stack-base`](https://github.com/RobertoRodriguezCarbonell/stack-base)
|
|
4
|
+
boilerplate — Next.js 16 + Auth.js + Drizzle/Neon + Resend + (optional) R2.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm create ropo my-app
|
|
10
|
+
# or: npm create ropo@latest my-app
|
|
11
|
+
# or: bun create ropo my-app
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Then follow the prompts. It will:
|
|
15
|
+
|
|
16
|
+
1. Clone the `stack-base` template (fresh git history).
|
|
17
|
+
2. Rename the brand to your project name.
|
|
18
|
+
3. Optionally `git init` + install dependencies.
|
|
19
|
+
|
|
20
|
+
Finally:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cd my-app
|
|
24
|
+
cp .env.example .env.local # fill in your credentials
|
|
25
|
+
pnpm db:migrate
|
|
26
|
+
pnpm dev
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Notes
|
|
30
|
+
|
|
31
|
+
- **Private template:** the default template repo is private, so cloning uses
|
|
32
|
+
your own git credentials (SSH or an https credential helper). Override the
|
|
33
|
+
source with the `ROPO_TEMPLATE` env var, e.g.:
|
|
34
|
+
```bash
|
|
35
|
+
ROPO_TEMPLATE=git@github.com:RobertoRodriguezCarbonell/stack-base.git pnpm create ropo my-app
|
|
36
|
+
```
|
|
37
|
+
- **Zero dependencies:** the CLI uses only Node.js built-ins (`node >= 20`).
|
|
38
|
+
- v1 scaffolds the `base` archetype only. Future archetypes (saas, landing,
|
|
39
|
+
internal-tool) will be added to the prompt as they exist.
|
|
40
|
+
|
|
41
|
+
## Local development
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
node index.js ../tmp-test-app # run the CLI without publishing
|
|
45
|
+
```
|
package/index.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process"
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs"
|
|
10
|
+
import { basename, join, resolve } from "node:path"
|
|
11
|
+
import { stdin as input, stdout as output } from "node:process"
|
|
12
|
+
import { createInterface } from "node:readline/promises"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* create-ropo — scaffolds a new project from the stack-base boilerplate.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* pnpm create ropo my-app (interactive)
|
|
19
|
+
* pnpm create ropo my-app -y (accept defaults: install + git)
|
|
20
|
+
* pnpm create ropo my-app --no-install --no-git
|
|
21
|
+
*
|
|
22
|
+
* v1 (base only): clone the template, strip its git history, rename the brand
|
|
23
|
+
* to your project name, optionally init git + install dependencies.
|
|
24
|
+
*
|
|
25
|
+
* The template defaults to the private stack-base repo, so cloning uses your
|
|
26
|
+
* own git credentials. Override the source with the ROPO_TEMPLATE env var.
|
|
27
|
+
*/
|
|
28
|
+
const TEMPLATE_REPO =
|
|
29
|
+
process.env.ROPO_TEMPLATE ??
|
|
30
|
+
"https://github.com/RobertoRodriguezCarbonell/stack-base.git"
|
|
31
|
+
|
|
32
|
+
const BRAND = "stack-base"
|
|
33
|
+
|
|
34
|
+
const c = {
|
|
35
|
+
dim: (s) => `\x1b[2m${s}\x1b[22m`,
|
|
36
|
+
bold: (s) => `\x1b[1m${s}\x1b[22m`,
|
|
37
|
+
green: (s) => `\x1b[32m${s}\x1b[39m`,
|
|
38
|
+
cyan: (s) => `\x1b[36m${s}\x1b[39m`,
|
|
39
|
+
red: (s) => `\x1b[31m${s}\x1b[39m`,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function die(msg) {
|
|
43
|
+
console.error("\n" + c.red("✗ " + msg) + "\n")
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function run(cmd, args, opts = {}) {
|
|
48
|
+
const r = spawnSync(cmd, args, { stdio: "inherit", ...opts })
|
|
49
|
+
if (r.error || r.status !== 0) {
|
|
50
|
+
die(`\`${cmd} ${args.join(" ")}\` failed.`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function detectPackageManager() {
|
|
55
|
+
const ua = process.env.npm_config_user_agent ?? ""
|
|
56
|
+
if (ua.startsWith("pnpm")) return "pnpm"
|
|
57
|
+
if (ua.startsWith("yarn")) return "yarn"
|
|
58
|
+
if (ua.startsWith("bun")) return "bun"
|
|
59
|
+
return "npm"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function slugify(name) {
|
|
63
|
+
return (
|
|
64
|
+
name
|
|
65
|
+
.trim()
|
|
66
|
+
.toLowerCase()
|
|
67
|
+
.replace(/[^a-z0-9-~]+/g, "-")
|
|
68
|
+
.replace(/^-+|-+$/g, "") || "my-app"
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const SKIP_DIRS = new Set([".git", "node_modules", ".next", "drizzle", ".vercel"])
|
|
73
|
+
const SKIP_FILES = new Set([
|
|
74
|
+
"pnpm-lock.yaml",
|
|
75
|
+
"package-lock.json",
|
|
76
|
+
"yarn.lock",
|
|
77
|
+
"bun.lockb",
|
|
78
|
+
])
|
|
79
|
+
const TEXT_EXT =
|
|
80
|
+
/\.(ts|tsx|js|jsx|mjs|cjs|mts|json|md|mdx|css|html|txt|yaml|yml)$/i
|
|
81
|
+
|
|
82
|
+
/** Replace every occurrence of `from` with `to` in text files under `dir`. */
|
|
83
|
+
function replaceInTree(dir, from, to) {
|
|
84
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
if (!SKIP_DIRS.has(entry.name)) {
|
|
87
|
+
replaceInTree(join(dir, entry.name), from, to)
|
|
88
|
+
}
|
|
89
|
+
} else if (
|
|
90
|
+
entry.isFile() &&
|
|
91
|
+
!SKIP_FILES.has(entry.name) &&
|
|
92
|
+
TEXT_EXT.test(entry.name)
|
|
93
|
+
) {
|
|
94
|
+
const file = join(dir, entry.name)
|
|
95
|
+
const content = readFileSync(file, "utf8")
|
|
96
|
+
if (content.includes(from)) {
|
|
97
|
+
writeFileSync(file, content.split(from).join(to))
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function main() {
|
|
104
|
+
const args = process.argv.slice(2)
|
|
105
|
+
const positionals = args.filter((a) => !a.startsWith("-"))
|
|
106
|
+
const has = (...names) => names.some((n) => args.includes(n))
|
|
107
|
+
|
|
108
|
+
const yes = has("-y", "--yes")
|
|
109
|
+
const interactive = Boolean(input.isTTY) && !yes
|
|
110
|
+
|
|
111
|
+
console.log(
|
|
112
|
+
"\n" + c.bold(c.cyan("create-ropo")) + c.dim(" scaffold from stack-base\n")
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const rl = interactive ? createInterface({ input, output }) : null
|
|
116
|
+
const ask = async (question, def) => {
|
|
117
|
+
if (!rl) return def
|
|
118
|
+
const a = (
|
|
119
|
+
await rl.question(`${question}${def ? c.dim(` (${def})`) : ""}: `)
|
|
120
|
+
).trim()
|
|
121
|
+
return a || def || ""
|
|
122
|
+
}
|
|
123
|
+
const askYesNo = async (question, def) => {
|
|
124
|
+
const a = await ask(`${question} ${def ? "[Y/n]" : "[y/N]"}`, "")
|
|
125
|
+
if (!a) return def
|
|
126
|
+
return a.toLowerCase().startsWith("y")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Target directory: positional arg, else prompt, else default.
|
|
130
|
+
const target = positionals[0] || (await ask("Project directory", "my-app"))
|
|
131
|
+
const dir = resolve(process.cwd(), target)
|
|
132
|
+
const name = slugify(basename(dir))
|
|
133
|
+
|
|
134
|
+
if (existsSync(dir) && readdirSync(dir).length > 0) {
|
|
135
|
+
rl?.close()
|
|
136
|
+
die(`Directory "${target}" already exists and is not empty.`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Install / git: flags win, otherwise prompt (or default when non-interactive).
|
|
140
|
+
const doInstall = has("--no-install")
|
|
141
|
+
? false
|
|
142
|
+
: has("--install") || yes
|
|
143
|
+
? true
|
|
144
|
+
: await askYesNo("Install dependencies now?", true)
|
|
145
|
+
const doGit = has("--no-git")
|
|
146
|
+
? false
|
|
147
|
+
: has("--git") || yes
|
|
148
|
+
? true
|
|
149
|
+
: await askYesNo("Initialize a git repository?", true)
|
|
150
|
+
rl?.close()
|
|
151
|
+
|
|
152
|
+
const pm = detectPackageManager()
|
|
153
|
+
|
|
154
|
+
console.log(c.dim(`\n→ Cloning template into ${target}/ …`))
|
|
155
|
+
run("git", ["clone", "--depth", "1", TEMPLATE_REPO, dir])
|
|
156
|
+
rmSync(join(dir, ".git"), { recursive: true, force: true })
|
|
157
|
+
|
|
158
|
+
console.log(c.dim(`→ Renaming "${BRAND}" → "${name}" …`))
|
|
159
|
+
replaceInTree(dir, BRAND, name)
|
|
160
|
+
|
|
161
|
+
if (doGit) {
|
|
162
|
+
console.log(c.dim("→ Initializing git …"))
|
|
163
|
+
run("git", ["init", "-q"], { cwd: dir })
|
|
164
|
+
run("git", ["add", "-A"], { cwd: dir })
|
|
165
|
+
run("git", ["commit", "-q", "-m", "chore: initialize from stack-base"], {
|
|
166
|
+
cwd: dir,
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (doInstall) {
|
|
171
|
+
console.log(c.dim(`→ Installing dependencies with ${pm} …\n`))
|
|
172
|
+
run(pm, ["install"], { cwd: dir })
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const runCmd = pm === "npm" ? "npm run" : pm
|
|
176
|
+
console.log("\n" + c.green("✓ Done!") + " Next steps:\n")
|
|
177
|
+
console.log(" " + c.cyan(`cd ${target}`))
|
|
178
|
+
if (!doInstall) console.log(" " + c.cyan(`${pm} install`))
|
|
179
|
+
console.log(
|
|
180
|
+
" " +
|
|
181
|
+
c.cyan("cp .env.example .env.local") +
|
|
182
|
+
c.dim(" # fill in your credentials")
|
|
183
|
+
)
|
|
184
|
+
console.log(" " + c.cyan(`${runCmd} db:migrate`))
|
|
185
|
+
console.log(" " + c.cyan(`${runCmd} dev`) + "\n")
|
|
186
|
+
console.log(
|
|
187
|
+
c.dim(" See README.md for the full setup (Neon, GitHub OAuth, Resend…).\n")
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
main().catch((error) => die(error?.message ?? String(error)))
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-ropo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a new project from the stack-base boilerplate (Next.js 16 + Auth.js + Drizzle/Neon + Resend).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-ropo": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=20"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"create",
|
|
17
|
+
"scaffold",
|
|
18
|
+
"boilerplate",
|
|
19
|
+
"starter",
|
|
20
|
+
"nextjs",
|
|
21
|
+
"next.js",
|
|
22
|
+
"authjs",
|
|
23
|
+
"drizzle",
|
|
24
|
+
"stack-base"
|
|
25
|
+
],
|
|
26
|
+
"author": "Roberto Rodriguez Carbonell",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/RobertoRodriguezCarbonell/create-ropo.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/RobertoRodriguezCarbonell/create-ropo/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/RobertoRodriguezCarbonell/create-ropo#readme",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|