create-kide-app 0.1.3 → 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 +319 -129
- 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,49 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import * as p from "@clack/prompts";
|
|
4
|
-
import {
|
|
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
|
-
import { promisify } from "node:util";
|
|
8
7
|
|
|
9
|
-
|
|
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
|
+
|
|
10
33
|
const REPO = "https://github.com/mhernesniemi/kide-cms.git";
|
|
11
34
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
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"];
|
|
15
37
|
|
|
16
|
-
|
|
17
|
-
for (const [k, v] of Object.entries(patch.dependencies?.add ?? {})) {
|
|
18
|
-
pkg.dependencies[k] = v;
|
|
19
|
-
}
|
|
20
|
-
for (const k of patch.dependencies?.remove ?? []) {
|
|
21
|
-
delete pkg.dependencies[k];
|
|
22
|
-
}
|
|
23
|
-
pkg.devDependencies ??= {};
|
|
24
|
-
for (const [k, v] of Object.entries(patch.devDependencies?.add ?? {})) {
|
|
25
|
-
pkg.devDependencies[k] = v;
|
|
26
|
-
}
|
|
27
|
-
for (const k of patch.devDependencies?.moveFromDependencies ?? []) {
|
|
28
|
-
if (pkg.dependencies[k]) {
|
|
29
|
-
pkg.devDependencies[k] = pkg.dependencies[k];
|
|
30
|
-
delete pkg.dependencies[k];
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
for (const [k, v] of Object.entries(patch.scripts ?? {})) {
|
|
34
|
-
pkg.scripts[k] = v;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
38
|
+
// --- Main ---
|
|
37
39
|
|
|
38
40
|
async function main() {
|
|
39
|
-
p.intro("Create Kide CMS Project");
|
|
41
|
+
p.intro("🪐 Create Kide CMS Project");
|
|
40
42
|
|
|
43
|
+
// 1. Project name
|
|
41
44
|
const projectName =
|
|
42
45
|
process.argv[2] ||
|
|
43
46
|
(await p.text({
|
|
44
47
|
message: "Project name",
|
|
45
48
|
placeholder: "my-cms-app",
|
|
46
|
-
validate: (value) =>
|
|
49
|
+
validate: (value) => {
|
|
50
|
+
if (!value) return "Project name is required";
|
|
51
|
+
},
|
|
47
52
|
}));
|
|
48
53
|
|
|
49
54
|
if (p.isCancel(projectName)) {
|
|
@@ -57,22 +62,28 @@ async function main() {
|
|
|
57
62
|
process.exit(1);
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
],
|
|
63
72
|
});
|
|
64
73
|
|
|
65
|
-
if (p.isCancel(
|
|
74
|
+
if (p.isCancel(target)) {
|
|
66
75
|
p.cancel("Setup cancelled.");
|
|
67
76
|
process.exit(0);
|
|
68
77
|
}
|
|
69
78
|
|
|
79
|
+
// 3. Demo content (local only — Cloudflare uses remote D1)
|
|
70
80
|
let seedDemo = false;
|
|
71
|
-
if (
|
|
81
|
+
if (target === "local") {
|
|
72
82
|
const seed = await p.confirm({
|
|
73
83
|
message: "Seed database with demo content?",
|
|
74
84
|
initialValue: false,
|
|
75
85
|
});
|
|
86
|
+
|
|
76
87
|
if (p.isCancel(seed)) {
|
|
77
88
|
p.cancel("Setup cancelled.");
|
|
78
89
|
process.exit(0);
|
|
@@ -82,94 +93,233 @@ async function main() {
|
|
|
82
93
|
|
|
83
94
|
const s = p.spinner();
|
|
84
95
|
|
|
85
|
-
// ---
|
|
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");
|
|
86
115
|
|
|
87
|
-
|
|
88
|
-
await run(`git clone --depth 1 ${REPO} "${projectDir}"`);
|
|
89
|
-
rmSync(path.join(projectDir, ".git"), { recursive: true, force: true });
|
|
90
|
-
s.stop("Cloned");
|
|
116
|
+
// --- Apply target-specific files ---
|
|
91
117
|
|
|
92
|
-
|
|
118
|
+
s.start(`Applying ${target} configuration`);
|
|
93
119
|
|
|
94
|
-
|
|
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
|
+
}
|
|
95
132
|
|
|
96
133
|
const pkgPath = path.join(projectDir, "package.json");
|
|
97
134
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
135
|
+
|
|
98
136
|
pkg.name = projectName;
|
|
99
|
-
pkg.version = "0.0.1";
|
|
100
137
|
|
|
101
|
-
|
|
138
|
+
if (target === "cloudflare") {
|
|
139
|
+
delete pkg.dependencies["@astrojs/node"];
|
|
140
|
+
pkg.dependencies["@astrojs/cloudflare"] = "^13.0.0";
|
|
102
141
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"
|
|
107
|
-
|
|
108
|
-
"src/cms/adapters/db.ts",
|
|
109
|
-
"src/cms/adapters/storage.ts",
|
|
110
|
-
"src/pages/uploads/[...path].ts",
|
|
111
|
-
];
|
|
112
|
-
for (const f of overlayFiles) {
|
|
113
|
-
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"];
|
|
114
147
|
}
|
|
148
|
+
delete pkg.dependencies["sharp"];
|
|
115
149
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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);
|
|
153
|
+
|
|
154
|
+
pkg.devDependencies.wrangler = "^4.0.0";
|
|
121
155
|
|
|
122
|
-
|
|
123
|
-
|
|
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";
|
|
124
160
|
}
|
|
125
161
|
|
|
126
|
-
rmSync(adaptersDir, { recursive: true, force: true });
|
|
127
162
|
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
128
163
|
|
|
129
|
-
|
|
164
|
+
rmSync(adaptersDir, { recursive: true, force: true });
|
|
165
|
+
|
|
166
|
+
s.stop("Configuration applied");
|
|
130
167
|
|
|
131
168
|
// --- Install dependencies ---
|
|
132
169
|
|
|
133
170
|
s.start("Installing dependencies");
|
|
134
171
|
try {
|
|
135
|
-
await
|
|
172
|
+
await runAsync(pm.install, projectDir);
|
|
136
173
|
s.stop("Dependencies installed");
|
|
137
174
|
} catch {
|
|
138
|
-
s.stop(
|
|
175
|
+
s.stop(`${pm.install} failed — run it manually`);
|
|
139
176
|
}
|
|
140
177
|
|
|
141
|
-
// ---
|
|
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
|
|
189
|
+
}
|
|
142
190
|
|
|
143
|
-
|
|
144
|
-
|
|
191
|
+
// --- Optional: create GitHub repository ---
|
|
192
|
+
|
|
193
|
+
if (gitInitialized) {
|
|
194
|
+
let ghAvailable = false;
|
|
145
195
|
try {
|
|
146
|
-
|
|
147
|
-
|
|
196
|
+
execSync("gh --version", { stdio: "pipe" });
|
|
197
|
+
execSync("gh auth status", { stdio: "pipe" });
|
|
198
|
+
ghAvailable = true;
|
|
148
199
|
} catch {
|
|
149
|
-
|
|
200
|
+
// gh not installed or not authenticated — skip the prompt
|
|
201
|
+
}
|
|
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
|
+
}
|
|
150
266
|
}
|
|
267
|
+
}
|
|
151
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") {
|
|
152
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;
|
|
153
286
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
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");
|
|
156
293
|
} catch {
|
|
157
|
-
s.stop("Schema
|
|
294
|
+
s.stop("Schema will be set up on first dev start");
|
|
158
295
|
}
|
|
159
|
-
|
|
160
296
|
s.start("Seeding demo content");
|
|
161
297
|
try {
|
|
162
|
-
|
|
298
|
+
execSync(`${pm.run} cms:seed`, { cwd: projectDir, stdio: "pipe" });
|
|
163
299
|
s.stop("Demo content seeded");
|
|
164
|
-
} catch {
|
|
300
|
+
} catch (err) {
|
|
165
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());
|
|
166
304
|
}
|
|
305
|
+
} else if (seedDemo && target === "cloudflare") {
|
|
306
|
+
p.note(
|
|
307
|
+
[
|
|
308
|
+
"Seeding for Cloudflare requires a D1 database.",
|
|
309
|
+
"",
|
|
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`,
|
|
314
|
+
].join("\n"),
|
|
315
|
+
"Seed manually",
|
|
316
|
+
);
|
|
167
317
|
}
|
|
168
318
|
|
|
169
319
|
// --- Cloudflare resource setup ---
|
|
170
320
|
|
|
171
|
-
const cf = { d1Created: false, r2Created: false, migrationsApplied: false };
|
|
172
|
-
if (cloudflare) {
|
|
321
|
+
const cf = { d1Created: false, r2Created: false, migrationsApplied: false, deployed: false, url: null };
|
|
322
|
+
if (target === "cloudflare") {
|
|
173
323
|
const setupNow = await p.confirm({
|
|
174
324
|
message: "Set up Cloudflare resources now? (creates D1 database and R2 bucket)",
|
|
175
325
|
initialValue: true,
|
|
@@ -179,17 +329,17 @@ async function main() {
|
|
|
179
329
|
// Check wrangler authentication
|
|
180
330
|
let authenticated = false;
|
|
181
331
|
try {
|
|
182
|
-
|
|
332
|
+
execSync(`${pm.exec} wrangler whoami`, { cwd: projectDir, stdio: "pipe" });
|
|
183
333
|
authenticated = true;
|
|
184
334
|
} catch {
|
|
185
335
|
p.note("You need to log in to Cloudflare first.", "Wrangler login required");
|
|
186
336
|
const doLogin = await p.confirm({ message: "Open browser to log in?", initialValue: true });
|
|
187
337
|
if (!p.isCancel(doLogin) && doLogin) {
|
|
188
338
|
try {
|
|
189
|
-
execSync(
|
|
339
|
+
execSync(`${pm.exec} wrangler login`, { cwd: projectDir, stdio: "inherit" });
|
|
190
340
|
authenticated = true;
|
|
191
341
|
} catch {
|
|
192
|
-
|
|
342
|
+
s.stop("Login failed");
|
|
193
343
|
}
|
|
194
344
|
}
|
|
195
345
|
}
|
|
@@ -199,16 +349,20 @@ async function main() {
|
|
|
199
349
|
let databaseId = null;
|
|
200
350
|
s.start("Creating D1 database");
|
|
201
351
|
try {
|
|
202
|
-
const
|
|
203
|
-
|
|
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*"([^"]+)"/);
|
|
204
357
|
if (match) databaseId = match[1];
|
|
205
358
|
cf.d1Created = true;
|
|
206
359
|
s.stop("D1 database created");
|
|
207
|
-
} catch {
|
|
360
|
+
} catch (err) {
|
|
208
361
|
// Already exists — look it up
|
|
209
362
|
try {
|
|
210
|
-
const
|
|
211
|
-
const
|
|
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`));
|
|
212
366
|
if (dbLine) {
|
|
213
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}/);
|
|
214
368
|
if (idMatch) databaseId = idMatch[0];
|
|
@@ -218,6 +372,7 @@ async function main() {
|
|
|
218
372
|
s.stop("D1 database already exists — using existing");
|
|
219
373
|
} else {
|
|
220
374
|
s.stop("D1 setup failed");
|
|
375
|
+
if (err.stderr) console.error(err.stderr.toString());
|
|
221
376
|
}
|
|
222
377
|
} catch {
|
|
223
378
|
s.stop("D1 setup failed");
|
|
@@ -228,87 +383,122 @@ async function main() {
|
|
|
228
383
|
if (databaseId) {
|
|
229
384
|
const wranglerPath = path.join(projectDir, "wrangler.toml");
|
|
230
385
|
let wranglerContent = readFileSync(wranglerPath, "utf-8");
|
|
231
|
-
wranglerContent = wranglerContent.replace(
|
|
232
|
-
/database_id = "" #[^\n]*/,
|
|
233
|
-
`database_id = "${databaseId}"`,
|
|
234
|
-
);
|
|
386
|
+
wranglerContent = wranglerContent.replace(/database_id = "" #[^\n]*/, `database_id = "${databaseId}"`);
|
|
235
387
|
writeFileSync(wranglerPath, wranglerContent);
|
|
236
388
|
}
|
|
237
389
|
|
|
238
390
|
// Create R2 bucket
|
|
239
391
|
s.start("Creating R2 bucket");
|
|
240
392
|
try {
|
|
241
|
-
|
|
393
|
+
execSync(`${pm.exec} wrangler r2 bucket create ${projectName}-assets`, { cwd: projectDir, stdio: "pipe" });
|
|
242
394
|
cf.r2Created = true;
|
|
243
395
|
s.stop("R2 bucket created");
|
|
244
396
|
} catch {
|
|
245
|
-
// Already exists is fine
|
|
246
397
|
cf.r2Created = true;
|
|
247
398
|
s.stop("R2 bucket already exists");
|
|
248
399
|
}
|
|
249
400
|
|
|
250
|
-
// Generate
|
|
401
|
+
// Generate migrations and apply to remote D1
|
|
251
402
|
if (databaseId) {
|
|
252
|
-
s.start("Generating CMS schema");
|
|
253
|
-
try {
|
|
254
|
-
await run("pnpm cms:generate", projectDir);
|
|
255
|
-
s.stop("Schema generated");
|
|
256
|
-
} catch {
|
|
257
|
-
s.stop("Schema generation failed");
|
|
258
|
-
}
|
|
259
|
-
|
|
260
403
|
s.start("Generating database migrations");
|
|
261
404
|
try {
|
|
262
|
-
|
|
405
|
+
execSync(`${pm.exec} drizzle-kit generate`, { cwd: projectDir, stdio: "pipe" });
|
|
263
406
|
s.stop("Migrations generated");
|
|
264
|
-
} catch {
|
|
407
|
+
} catch (err) {
|
|
265
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));
|
|
266
411
|
}
|
|
267
412
|
|
|
268
413
|
s.start("Applying migrations to remote D1");
|
|
269
414
|
try {
|
|
270
|
-
|
|
415
|
+
execSync(`${pm.exec} wrangler d1 migrations apply ${projectName}-db --remote`, {
|
|
416
|
+
cwd: projectDir,
|
|
417
|
+
stdio: "pipe",
|
|
418
|
+
input: "y\n",
|
|
419
|
+
});
|
|
271
420
|
cf.migrationsApplied = true;
|
|
272
421
|
s.stop("Migrations applied");
|
|
273
422
|
} catch {
|
|
274
423
|
s.stop("Migration apply failed — run manually with: wrangler d1 migrations apply --remote");
|
|
275
424
|
}
|
|
276
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
|
+
}
|
|
277
448
|
}
|
|
278
449
|
}
|
|
279
450
|
}
|
|
280
451
|
|
|
281
452
|
// --- Done ---
|
|
282
453
|
|
|
283
|
-
if (
|
|
284
|
-
const lines = [`cd ${projectName}`];
|
|
285
|
-
const remaining = [];
|
|
286
|
-
if (!cf.d1Created) {
|
|
287
|
-
remaining.push(
|
|
288
|
-
` pnpm wrangler d1 create ${projectName}-db`,
|
|
289
|
-
" # Paste the database_id into wrangler.toml",
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
if (!cf.r2Created) {
|
|
293
|
-
remaining.push(` pnpm wrangler r2 bucket create ${projectName}-assets`);
|
|
294
|
-
}
|
|
295
|
-
if (!cf.migrationsApplied) {
|
|
296
|
-
remaining.push(` pnpm wrangler d1 migrations apply ${projectName}-db --remote`);
|
|
297
|
-
}
|
|
298
|
-
if (remaining.length > 0) {
|
|
299
|
-
lines.push("", "Remaining setup:", ...remaining);
|
|
300
|
-
}
|
|
301
|
-
lines.push("", "Local development:", " pnpm dev", "", "Deploy:", " pnpm deploy");
|
|
302
|
-
p.note(lines.join("\n"), "Next steps");
|
|
303
|
-
p.outro("Project created!");
|
|
304
|
-
} else {
|
|
454
|
+
if (target === "local") {
|
|
305
455
|
p.outro("Starting dev server...");
|
|
306
456
|
try {
|
|
307
|
-
execSync(
|
|
457
|
+
execSync(`${pm.run} dev`, { cwd: projectDir, stdio: "inherit" });
|
|
308
458
|
} catch {
|
|
309
459
|
console.log(`\n Project directory: ${projectDir}`);
|
|
310
460
|
console.log(` To start again: cd ${projectName} && pnpm dev\n`);
|
|
311
461
|
}
|
|
462
|
+
} else {
|
|
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
|
+
}
|
|
312
502
|
}
|
|
313
503
|
}
|
|
314
504
|
|