create-kide-app 0.1.2 → 0.1.4
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 +11 -12
- package/index.js +414 -91
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,28 +5,27 @@ Scaffold a new [Kide CMS](https://github.com/mhernesniemi/kide-cms) project.
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
+
npm create kide-app my-project
|
|
9
|
+
# or
|
|
8
10
|
pnpm create kide-app my-project
|
|
9
11
|
# or
|
|
10
|
-
|
|
12
|
+
bunx create-kide-app my-project
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
The CLI
|
|
15
|
+
The CLI will guide you through:
|
|
14
16
|
|
|
15
17
|
1. **Project name** — directory to create
|
|
16
|
-
2. **Deploy target** — Node.js
|
|
18
|
+
2. **Deploy target** — Local/Node.js or Cloudflare
|
|
19
|
+
3. **Demo content** — optionally seed the database
|
|
17
20
|
|
|
18
21
|
## What it does
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
For Cloudflare projects, the CLI prints the remaining manual steps (create D1/R2, apply migrations).
|
|
23
|
+
- Downloads the latest Kide CMS template from GitHub
|
|
24
|
+
- Applies platform-specific configuration (Node.js adapter, Cloudflare D1/R2, etc.)
|
|
25
|
+
- Installs dependencies
|
|
26
|
+
- Generates the CMS schema
|
|
27
|
+
- Optionally seeds demo content
|
|
27
28
|
|
|
28
29
|
## Requirements
|
|
29
30
|
|
|
30
31
|
- Node.js >= 22.12.0
|
|
31
|
-
- `git` on PATH
|
|
32
|
-
- `pnpm`
|
package/index.js
CHANGED
|
@@ -1,43 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import * as p from "@clack/prompts";
|
|
4
|
-
import { execSync } from "node:child_process";
|
|
5
|
-
import { cpSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { execSync, spawn } from "node:child_process";
|
|
5
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
|
|
8
|
+
// Async spawn wrapper so long-running commands don't block clack spinners
|
|
9
|
+
const runAsync = (cmd, cwd) =>
|
|
10
|
+
new Promise((resolve, reject) => {
|
|
11
|
+
const child = spawn(cmd, { cwd, shell: true });
|
|
12
|
+
let stdout = "";
|
|
13
|
+
let stderr = "";
|
|
14
|
+
child.stdout.on("data", (d) => (stdout += d.toString()));
|
|
15
|
+
child.stderr.on("data", (d) => (stderr += d.toString()));
|
|
16
|
+
child.on("close", (code) => {
|
|
17
|
+
if (code === 0) resolve(stdout);
|
|
18
|
+
else {
|
|
19
|
+
const err = new Error(`Command failed: ${cmd}`);
|
|
20
|
+
err.stderr = stderr;
|
|
21
|
+
err.stdout = stdout;
|
|
22
|
+
reject(err);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// --- Package manager detection ---
|
|
28
|
+
|
|
29
|
+
const pm = { name: "pnpm", exec: "pnpm exec", dlx: "pnpm dlx", run: "pnpm", install: "pnpm install" };
|
|
30
|
+
|
|
31
|
+
// --- Template repo ---
|
|
32
|
+
|
|
8
33
|
const REPO = "https://github.com/mhernesniemi/kide-cms.git";
|
|
9
34
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
for (const k of patch.dependencies?.remove ?? []) {
|
|
15
|
-
delete pkg.dependencies[k];
|
|
16
|
-
}
|
|
17
|
-
pkg.devDependencies ??= {};
|
|
18
|
-
for (const [k, v] of Object.entries(patch.devDependencies?.add ?? {})) {
|
|
19
|
-
pkg.devDependencies[k] = v;
|
|
20
|
-
}
|
|
21
|
-
for (const k of patch.devDependencies?.moveFromDependencies ?? []) {
|
|
22
|
-
if (pkg.dependencies[k]) {
|
|
23
|
-
pkg.devDependencies[k] = pkg.dependencies[k];
|
|
24
|
-
delete pkg.dependencies[k];
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
for (const [k, v] of Object.entries(patch.scripts ?? {})) {
|
|
28
|
-
pkg.scripts[k] = v;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
35
|
+
// Files from the kide-cms repo that shouldn't leak into scaffolded projects.
|
|
36
|
+
const CLEANUP = ["docs", "CLAUDE.md", ".claude", "data", ".cms-data", "dist", ".astro", ".DS_Store"];
|
|
37
|
+
|
|
38
|
+
// --- Main ---
|
|
31
39
|
|
|
32
40
|
async function main() {
|
|
33
|
-
p.intro("Create Kide CMS Project");
|
|
41
|
+
p.intro("🪐 Create Kide CMS Project");
|
|
34
42
|
|
|
43
|
+
// 1. Project name
|
|
35
44
|
const projectName =
|
|
36
45
|
process.argv[2] ||
|
|
37
46
|
(await p.text({
|
|
38
47
|
message: "Project name",
|
|
39
48
|
placeholder: "my-cms-app",
|
|
40
|
-
validate: (value) =>
|
|
49
|
+
validate: (value) => {
|
|
50
|
+
if (!value) return "Project name is required";
|
|
51
|
+
},
|
|
41
52
|
}));
|
|
42
53
|
|
|
43
54
|
if (p.isCancel(projectName)) {
|
|
@@ -51,22 +62,28 @@ async function main() {
|
|
|
51
62
|
process.exit(1);
|
|
52
63
|
}
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
// 2. Deploy target
|
|
66
|
+
const target = await p.select({
|
|
67
|
+
message: "Where will you deploy?",
|
|
68
|
+
options: [
|
|
69
|
+
{ label: "Local / Node.js", value: "local" },
|
|
70
|
+
{ label: "Cloudflare", value: "cloudflare" },
|
|
71
|
+
],
|
|
57
72
|
});
|
|
58
73
|
|
|
59
|
-
if (p.isCancel(
|
|
74
|
+
if (p.isCancel(target)) {
|
|
60
75
|
p.cancel("Setup cancelled.");
|
|
61
76
|
process.exit(0);
|
|
62
77
|
}
|
|
63
78
|
|
|
79
|
+
// 3. Demo content (local only — Cloudflare uses remote D1)
|
|
64
80
|
let seedDemo = false;
|
|
65
|
-
if (
|
|
81
|
+
if (target === "local") {
|
|
66
82
|
const seed = await p.confirm({
|
|
67
83
|
message: "Seed database with demo content?",
|
|
68
84
|
initialValue: false,
|
|
69
85
|
});
|
|
86
|
+
|
|
70
87
|
if (p.isCancel(seed)) {
|
|
71
88
|
p.cancel("Setup cancelled.");
|
|
72
89
|
process.exit(0);
|
|
@@ -76,106 +93,412 @@ async function main() {
|
|
|
76
93
|
|
|
77
94
|
const s = p.spinner();
|
|
78
95
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
// --- Scaffold via git clone ---
|
|
97
|
+
|
|
98
|
+
s.start(`Scaffolding project (using ${pm.name})`);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
execSync(`git clone --depth 1 ${REPO} "${projectDir}"`, { stdio: "pipe" });
|
|
102
|
+
rmSync(path.join(projectDir, ".git"), { recursive: true, force: true });
|
|
103
|
+
} catch {
|
|
104
|
+
s.stop("Failed to download template.");
|
|
105
|
+
p.cancel("Check your network connection.");
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Remove files that shouldn't be in the scaffold
|
|
110
|
+
for (const f of CLEANUP) {
|
|
111
|
+
rmSync(path.join(projectDir, f), { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
s.stop("Project scaffolded");
|
|
115
|
+
|
|
116
|
+
// --- Apply target-specific files ---
|
|
117
|
+
|
|
118
|
+
s.start(`Applying ${target} configuration`);
|
|
119
|
+
|
|
120
|
+
const adaptersDir = path.join(projectDir, "adapters");
|
|
121
|
+
const targetDir = path.join(adaptersDir, target);
|
|
122
|
+
|
|
123
|
+
if (target === "cloudflare") {
|
|
124
|
+
cpSync(path.join(targetDir, "astro.config.mjs"), path.join(projectDir, "astro.config.mjs"));
|
|
125
|
+
cpSync(path.join(targetDir, "src/cms/adapters/db.ts"), path.join(projectDir, "src/cms/adapters/db.ts"));
|
|
126
|
+
cpSync(path.join(targetDir, "drizzle.config.ts"), path.join(projectDir, "drizzle.config.ts"));
|
|
127
|
+
cpSync(path.join(targetDir, "src/cms/adapters/storage.ts"), path.join(projectDir, "src/cms/adapters/storage.ts"));
|
|
128
|
+
const uploadsRouteDir = path.join(projectDir, "src/pages/uploads");
|
|
129
|
+
mkdirSync(uploadsRouteDir, { recursive: true });
|
|
130
|
+
cpSync(path.join(targetDir, "src/pages/uploads/[...path].ts"), path.join(uploadsRouteDir, "[...path].ts"));
|
|
131
|
+
}
|
|
83
132
|
|
|
84
133
|
const pkgPath = path.join(projectDir, "package.json");
|
|
85
134
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
135
|
+
|
|
86
136
|
pkg.name = projectName;
|
|
87
|
-
pkg.version = "0.0.1";
|
|
88
137
|
|
|
89
|
-
|
|
138
|
+
if (target === "cloudflare") {
|
|
139
|
+
delete pkg.dependencies["@astrojs/node"];
|
|
140
|
+
pkg.dependencies["@astrojs/cloudflare"] = "^13.0.0";
|
|
90
141
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"astro.config.mjs",
|
|
97
|
-
"drizzle.config.ts",
|
|
98
|
-
"src/cms/adapters/db.ts",
|
|
99
|
-
"src/cms/adapters/storage.ts",
|
|
100
|
-
"src/pages/uploads/[...path].ts",
|
|
101
|
-
];
|
|
102
|
-
for (const f of overlayFiles) {
|
|
103
|
-
cpSync(path.join(cfDir, f), path.join(projectDir, f));
|
|
142
|
+
// Move better-sqlite3 to devDependencies — drizzle-kit needs it to push schema to local D1
|
|
143
|
+
if (pkg.dependencies["better-sqlite3"]) {
|
|
144
|
+
if (!pkg.devDependencies) pkg.devDependencies = {};
|
|
145
|
+
pkg.devDependencies["better-sqlite3"] = pkg.dependencies["better-sqlite3"];
|
|
146
|
+
delete pkg.dependencies["better-sqlite3"];
|
|
104
147
|
}
|
|
148
|
+
delete pkg.dependencies["sharp"];
|
|
105
149
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
);
|
|
110
|
-
writeFileSync(path.join(projectDir, "wrangler.toml"), wrangler);
|
|
150
|
+
let wranglerContent = readFileSync(path.join(targetDir, "wrangler.toml"), "utf-8");
|
|
151
|
+
wranglerContent = wranglerContent.replaceAll("{{PROJECT_NAME}}", projectName);
|
|
152
|
+
writeFileSync(path.join(projectDir, "wrangler.toml"), wranglerContent);
|
|
111
153
|
|
|
112
|
-
|
|
113
|
-
applyPackagePatch(pkg, patch);
|
|
154
|
+
pkg.devDependencies.wrangler = "^4.0.0";
|
|
114
155
|
|
|
115
|
-
|
|
156
|
+
pkg.scripts.dev = "astro dev";
|
|
157
|
+
pkg.scripts.build = "astro build";
|
|
158
|
+
pkg.scripts.preview = "astro build && wrangler dev --config dist/server/wrangler.json";
|
|
159
|
+
pkg.scripts.deploy = "astro build && wrangler deploy --config dist/server/wrangler.json";
|
|
116
160
|
}
|
|
117
161
|
|
|
118
|
-
rmSync(adaptersDir, { recursive: true, force: true });
|
|
119
162
|
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
120
163
|
|
|
164
|
+
rmSync(adaptersDir, { recursive: true, force: true });
|
|
165
|
+
|
|
166
|
+
s.stop("Configuration applied");
|
|
167
|
+
|
|
168
|
+
// --- Install dependencies ---
|
|
169
|
+
|
|
121
170
|
s.start("Installing dependencies");
|
|
122
171
|
try {
|
|
123
|
-
|
|
172
|
+
await runAsync(pm.install, projectDir);
|
|
124
173
|
s.stop("Dependencies installed");
|
|
125
174
|
} catch {
|
|
126
|
-
s.stop(
|
|
175
|
+
s.stop(`${pm.install} failed — run it manually`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --- Initialize git repository ---
|
|
179
|
+
|
|
180
|
+
let gitInitialized = false;
|
|
181
|
+
try {
|
|
182
|
+
execSync("git init -q && git add . && git commit -q -m 'Initial commit from create-kide-app'", {
|
|
183
|
+
cwd: projectDir,
|
|
184
|
+
stdio: "pipe",
|
|
185
|
+
});
|
|
186
|
+
gitInitialized = true;
|
|
187
|
+
} catch {
|
|
188
|
+
// git not available — silently skip
|
|
127
189
|
}
|
|
128
190
|
|
|
129
|
-
|
|
130
|
-
|
|
191
|
+
// --- Optional: create GitHub repository ---
|
|
192
|
+
|
|
193
|
+
if (gitInitialized) {
|
|
194
|
+
let ghAvailable = false;
|
|
131
195
|
try {
|
|
132
|
-
execSync("
|
|
133
|
-
|
|
196
|
+
execSync("gh --version", { stdio: "pipe" });
|
|
197
|
+
execSync("gh auth status", { stdio: "pipe" });
|
|
198
|
+
ghAvailable = true;
|
|
134
199
|
} catch {
|
|
135
|
-
|
|
200
|
+
// gh not installed or not authenticated — skip the prompt
|
|
136
201
|
}
|
|
137
202
|
|
|
203
|
+
if (ghAvailable) {
|
|
204
|
+
const createRepo = await p.confirm({
|
|
205
|
+
message: "Create a GitHub repository for this project?",
|
|
206
|
+
initialValue: false,
|
|
207
|
+
});
|
|
208
|
+
if (!p.isCancel(createRepo) && createRepo) {
|
|
209
|
+
// Get the GitHub username so we can check repo availability
|
|
210
|
+
let ghUser = "";
|
|
211
|
+
try {
|
|
212
|
+
ghUser = execSync("gh api user --jq .login", { stdio: "pipe" }).toString().trim();
|
|
213
|
+
} catch {
|
|
214
|
+
// ignore
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Prompt for repo name, validate it doesn't already exist
|
|
218
|
+
let repoName = null;
|
|
219
|
+
while (true) {
|
|
220
|
+
const input = await p.text({
|
|
221
|
+
message: "Repository name",
|
|
222
|
+
initialValue: projectName,
|
|
223
|
+
validate: (value) => {
|
|
224
|
+
if (!value) return "Repository name is required";
|
|
225
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(value)) return "Only letters, numbers, dots, hyphens, and underscores";
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
if (p.isCancel(input)) break;
|
|
229
|
+
|
|
230
|
+
if (ghUser) {
|
|
231
|
+
try {
|
|
232
|
+
execSync(`gh repo view ${ghUser}/${input}`, { stdio: "pipe" });
|
|
233
|
+
p.note(`A repository named "${input}" already exists. Pick a different name.`, "Name taken");
|
|
234
|
+
continue;
|
|
235
|
+
} catch {
|
|
236
|
+
// Repo doesn't exist — name is free
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
repoName = input;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (repoName) {
|
|
244
|
+
const visibility = await p.select({
|
|
245
|
+
message: "Repository visibility",
|
|
246
|
+
options: [
|
|
247
|
+
{ label: "Private", value: "--private" },
|
|
248
|
+
{ label: "Public", value: "--public" },
|
|
249
|
+
],
|
|
250
|
+
});
|
|
251
|
+
if (!p.isCancel(visibility)) {
|
|
252
|
+
s.start("Creating GitHub repository");
|
|
253
|
+
try {
|
|
254
|
+
execSync(`gh repo create ${repoName} ${visibility} --source=. --push`, {
|
|
255
|
+
cwd: projectDir,
|
|
256
|
+
stdio: "pipe",
|
|
257
|
+
});
|
|
258
|
+
s.stop("GitHub repository created and pushed");
|
|
259
|
+
} catch (err) {
|
|
260
|
+
s.stop("GitHub repository creation failed");
|
|
261
|
+
if (err.stderr) console.error(err.stderr.toString().slice(-500));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// --- Generate schema ---
|
|
270
|
+
|
|
271
|
+
s.start("Generating CMS schema");
|
|
272
|
+
try {
|
|
273
|
+
execSync(`${pm.run} cms:generate`, { cwd: projectDir, stdio: "pipe" });
|
|
274
|
+
s.stop("Schema generated");
|
|
275
|
+
} catch {
|
|
276
|
+
s.stop("Schema generation failed — run `cms:generate` manually");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// --- Seed demo content ---
|
|
280
|
+
|
|
281
|
+
if (seedDemo && target === "local") {
|
|
138
282
|
s.start("Pushing schema to database");
|
|
283
|
+
// Ensure data/ directory exists — drizzle-kit push silently exits 0 if it can't open the file
|
|
284
|
+
mkdirSync(path.join(projectDir, "data"), { recursive: true });
|
|
285
|
+
let pushOk = false;
|
|
139
286
|
try {
|
|
140
|
-
execSync(
|
|
141
|
-
|
|
287
|
+
const out = execSync(`${pm.exec} drizzle-kit push --force`, {
|
|
288
|
+
cwd: projectDir,
|
|
289
|
+
stdio: "pipe",
|
|
290
|
+
}).toString();
|
|
291
|
+
pushOk = !out.includes("Error:") && out.includes("Changes applied");
|
|
292
|
+
s.stop(pushOk ? "Schema pushed" : "Schema push failed — run `pnpm exec drizzle-kit push` manually");
|
|
142
293
|
} catch {
|
|
143
|
-
s.stop("Schema
|
|
294
|
+
s.stop("Schema will be set up on first dev start");
|
|
144
295
|
}
|
|
145
|
-
|
|
146
296
|
s.start("Seeding demo content");
|
|
147
297
|
try {
|
|
148
|
-
execSync(
|
|
298
|
+
execSync(`${pm.run} cms:seed`, { cwd: projectDir, stdio: "pipe" });
|
|
149
299
|
s.stop("Demo content seeded");
|
|
150
|
-
} catch {
|
|
300
|
+
} catch (err) {
|
|
151
301
|
s.stop("Seeding failed — run `pnpm cms:seed` manually");
|
|
302
|
+
if (err.stderr) console.error(err.stderr.toString());
|
|
303
|
+
if (err.stdout) console.error(err.stdout.toString());
|
|
152
304
|
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (cloudflare) {
|
|
305
|
+
} else if (seedDemo && target === "cloudflare") {
|
|
156
306
|
p.note(
|
|
157
307
|
[
|
|
158
|
-
|
|
159
|
-
"",
|
|
160
|
-
"1. Create Cloudflare resources:",
|
|
161
|
-
` pnpm wrangler d1 create ${projectName}-db`,
|
|
162
|
-
" # Paste the database_id into wrangler.toml",
|
|
163
|
-
` pnpm wrangler r2 bucket create ${projectName}-assets`,
|
|
308
|
+
"Seeding for Cloudflare requires a D1 database.",
|
|
164
309
|
"",
|
|
165
|
-
|
|
166
|
-
"
|
|
167
|
-
`
|
|
168
|
-
|
|
169
|
-
"3. Run locally or deploy:",
|
|
170
|
-
" pnpm dev",
|
|
171
|
-
" pnpm deploy",
|
|
310
|
+
` ${pm.dlx} wrangler d1 create ${projectName}-db`,
|
|
311
|
+
" # Add the database_id to wrangler.toml",
|
|
312
|
+
` ${pm.dlx} wrangler d1 migrations apply ${projectName}-db --local`,
|
|
313
|
+
` ${pm.run} cms:seed`,
|
|
172
314
|
].join("\n"),
|
|
173
|
-
"
|
|
315
|
+
"Seed manually",
|
|
174
316
|
);
|
|
175
|
-
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// --- Cloudflare resource setup ---
|
|
320
|
+
|
|
321
|
+
const cf = { d1Created: false, r2Created: false, migrationsApplied: false, deployed: false, url: null };
|
|
322
|
+
if (target === "cloudflare") {
|
|
323
|
+
const setupNow = await p.confirm({
|
|
324
|
+
message: "Set up Cloudflare resources now? (creates D1 database and R2 bucket)",
|
|
325
|
+
initialValue: true,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (!p.isCancel(setupNow) && setupNow) {
|
|
329
|
+
// Check wrangler authentication
|
|
330
|
+
let authenticated = false;
|
|
331
|
+
try {
|
|
332
|
+
execSync(`${pm.exec} wrangler whoami`, { cwd: projectDir, stdio: "pipe" });
|
|
333
|
+
authenticated = true;
|
|
334
|
+
} catch {
|
|
335
|
+
p.note("You need to log in to Cloudflare first.", "Wrangler login required");
|
|
336
|
+
const doLogin = await p.confirm({ message: "Open browser to log in?", initialValue: true });
|
|
337
|
+
if (!p.isCancel(doLogin) && doLogin) {
|
|
338
|
+
try {
|
|
339
|
+
execSync(`${pm.exec} wrangler login`, { cwd: projectDir, stdio: "inherit" });
|
|
340
|
+
authenticated = true;
|
|
341
|
+
} catch {
|
|
342
|
+
s.stop("Login failed");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (authenticated) {
|
|
348
|
+
// Create D1 database
|
|
349
|
+
let databaseId = null;
|
|
350
|
+
s.start("Creating D1 database");
|
|
351
|
+
try {
|
|
352
|
+
const output = execSync(`${pm.exec} wrangler d1 create ${projectName}-db`, {
|
|
353
|
+
cwd: projectDir,
|
|
354
|
+
stdio: "pipe",
|
|
355
|
+
}).toString();
|
|
356
|
+
const match = output.match(/database_id\s*=\s*"([^"]+)"/);
|
|
357
|
+
if (match) databaseId = match[1];
|
|
358
|
+
cf.d1Created = true;
|
|
359
|
+
s.stop("D1 database created");
|
|
360
|
+
} catch (err) {
|
|
361
|
+
// Already exists — look it up
|
|
362
|
+
try {
|
|
363
|
+
const listOutput = execSync(`${pm.exec} wrangler d1 list`, { cwd: projectDir, stdio: "pipe" }).toString();
|
|
364
|
+
const lines = listOutput.split("\n");
|
|
365
|
+
const dbLine = lines.find((l) => l.includes(`${projectName}-db`));
|
|
366
|
+
if (dbLine) {
|
|
367
|
+
const idMatch = dbLine.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
|
|
368
|
+
if (idMatch) databaseId = idMatch[0];
|
|
369
|
+
}
|
|
370
|
+
if (databaseId) {
|
|
371
|
+
cf.d1Created = true;
|
|
372
|
+
s.stop("D1 database already exists — using existing");
|
|
373
|
+
} else {
|
|
374
|
+
s.stop("D1 setup failed");
|
|
375
|
+
if (err.stderr) console.error(err.stderr.toString());
|
|
376
|
+
}
|
|
377
|
+
} catch {
|
|
378
|
+
s.stop("D1 setup failed");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Update wrangler.toml with database_id
|
|
383
|
+
if (databaseId) {
|
|
384
|
+
const wranglerPath = path.join(projectDir, "wrangler.toml");
|
|
385
|
+
let wranglerContent = readFileSync(wranglerPath, "utf-8");
|
|
386
|
+
wranglerContent = wranglerContent.replace(/database_id = "" #[^\n]*/, `database_id = "${databaseId}"`);
|
|
387
|
+
writeFileSync(wranglerPath, wranglerContent);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Create R2 bucket
|
|
391
|
+
s.start("Creating R2 bucket");
|
|
392
|
+
try {
|
|
393
|
+
execSync(`${pm.exec} wrangler r2 bucket create ${projectName}-assets`, { cwd: projectDir, stdio: "pipe" });
|
|
394
|
+
cf.r2Created = true;
|
|
395
|
+
s.stop("R2 bucket created");
|
|
396
|
+
} catch {
|
|
397
|
+
cf.r2Created = true;
|
|
398
|
+
s.stop("R2 bucket already exists");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Generate migrations and apply to remote D1
|
|
402
|
+
if (databaseId) {
|
|
403
|
+
s.start("Generating database migrations");
|
|
404
|
+
try {
|
|
405
|
+
execSync(`${pm.exec} drizzle-kit generate`, { cwd: projectDir, stdio: "pipe" });
|
|
406
|
+
s.stop("Migrations generated");
|
|
407
|
+
} catch (err) {
|
|
408
|
+
s.stop("Migration generation failed");
|
|
409
|
+
if (err.stderr) console.error(err.stderr.toString().slice(-800));
|
|
410
|
+
if (err.stdout) console.error(err.stdout.toString().slice(-800));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
s.start("Applying migrations to remote D1");
|
|
414
|
+
try {
|
|
415
|
+
execSync(`${pm.exec} wrangler d1 migrations apply ${projectName}-db --remote`, {
|
|
416
|
+
cwd: projectDir,
|
|
417
|
+
stdio: "pipe",
|
|
418
|
+
input: "y\n",
|
|
419
|
+
});
|
|
420
|
+
cf.migrationsApplied = true;
|
|
421
|
+
s.stop("Migrations applied");
|
|
422
|
+
} catch {
|
|
423
|
+
s.stop("Migration apply failed — run manually with: wrangler d1 migrations apply --remote");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Deploy to Cloudflare
|
|
428
|
+
if (cf.migrationsApplied) {
|
|
429
|
+
const doDeploy = await p.confirm({
|
|
430
|
+
message: "Deploy to Cloudflare now?",
|
|
431
|
+
initialValue: true,
|
|
432
|
+
});
|
|
433
|
+
if (!p.isCancel(doDeploy) && doDeploy) {
|
|
434
|
+
s.start("Building and deploying to Cloudflare");
|
|
435
|
+
try {
|
|
436
|
+
const deployOutput = await runAsync(`${pm.run} run deploy`, projectDir);
|
|
437
|
+
const urlMatch = deployOutput.match(/https:\/\/[^\s]+\.workers\.dev/);
|
|
438
|
+
if (urlMatch) cf.url = urlMatch[0];
|
|
439
|
+
cf.deployed = true;
|
|
440
|
+
s.stop("Deployed to Cloudflare");
|
|
441
|
+
} catch (err) {
|
|
442
|
+
s.stop("Deploy failed — run manually with: pnpm run deploy");
|
|
443
|
+
if (err.stderr) console.error(err.stderr.slice(-1500));
|
|
444
|
+
if (err.stdout) console.error(err.stdout.slice(-1500));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// --- Done ---
|
|
453
|
+
|
|
454
|
+
if (target === "local") {
|
|
455
|
+
p.outro("Starting dev server...");
|
|
456
|
+
try {
|
|
457
|
+
execSync(`${pm.run} dev`, { cwd: projectDir, stdio: "inherit" });
|
|
458
|
+
} catch {
|
|
459
|
+
console.log(`\n Project directory: ${projectDir}`);
|
|
460
|
+
console.log(` To start again: cd ${projectName} && pnpm dev\n`);
|
|
461
|
+
}
|
|
176
462
|
} else {
|
|
177
|
-
|
|
178
|
-
|
|
463
|
+
if (cf.deployed && cf.url) {
|
|
464
|
+
p.note(
|
|
465
|
+
[
|
|
466
|
+
`Live at: ${cf.url}`,
|
|
467
|
+
`Admin: ${cf.url}/admin`,
|
|
468
|
+
"",
|
|
469
|
+
`cd ${projectName}`,
|
|
470
|
+
"",
|
|
471
|
+
"Local development:",
|
|
472
|
+
` ${pm.run} dev`,
|
|
473
|
+
"",
|
|
474
|
+
"Redeploy:",
|
|
475
|
+
" pnpm run deploy",
|
|
476
|
+
].join("\n"),
|
|
477
|
+
"🎉 Your Kide CMS is live",
|
|
478
|
+
);
|
|
479
|
+
p.outro("Project created!");
|
|
480
|
+
} else {
|
|
481
|
+
const lines = [`cd ${projectName}`];
|
|
482
|
+
const remaining = [];
|
|
483
|
+
if (!cf.d1Created) {
|
|
484
|
+
remaining.push(` ${pm.dlx} wrangler d1 create ${projectName}-db`, " # Copy the database_id to wrangler.toml");
|
|
485
|
+
}
|
|
486
|
+
if (!cf.r2Created) {
|
|
487
|
+
remaining.push(` ${pm.dlx} wrangler r2 bucket create ${projectName}-assets`);
|
|
488
|
+
}
|
|
489
|
+
if (!cf.migrationsApplied) {
|
|
490
|
+
remaining.push(` ${pm.dlx} wrangler d1 migrations apply ${projectName}-db --remote`);
|
|
491
|
+
}
|
|
492
|
+
if (!cf.deployed) {
|
|
493
|
+
remaining.push(` ${pm.run} run deploy`);
|
|
494
|
+
}
|
|
495
|
+
if (remaining.length > 0) {
|
|
496
|
+
lines.push("", "Remaining setup:", ...remaining);
|
|
497
|
+
}
|
|
498
|
+
lines.push("", "Local development:", ` ${pm.run} dev`);
|
|
499
|
+
p.note(lines.join("\n"), "Next steps");
|
|
500
|
+
p.outro("Project created!");
|
|
501
|
+
}
|
|
179
502
|
}
|
|
180
503
|
}
|
|
181
504
|
|