create-kide-app 0.1.3 → 0.1.5
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 +327 -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,241 @@ 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}`, { cwd: projectDir, stdio: "pipe" });
|
|
255
|
+
|
|
256
|
+
// Use SSH for the remote (works with the user's existing SSH keys;
|
|
257
|
+
// avoids HTTPS credential prompts when gh's git_protocol defaults to https).
|
|
258
|
+
execSync(`git remote add origin git@github.com:${ghUser}/${repoName}.git`, {
|
|
259
|
+
cwd: projectDir,
|
|
260
|
+
stdio: "pipe",
|
|
261
|
+
});
|
|
262
|
+
execSync("git branch -M main && git push -u origin main", {
|
|
263
|
+
cwd: projectDir,
|
|
264
|
+
stdio: "pipe",
|
|
265
|
+
});
|
|
266
|
+
s.stop("GitHub repository created and pushed");
|
|
267
|
+
} catch (err) {
|
|
268
|
+
s.stop("GitHub repository creation failed");
|
|
269
|
+
if (err.stderr) console.error(err.stderr.toString().slice(-500));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
150
274
|
}
|
|
275
|
+
}
|
|
151
276
|
|
|
277
|
+
// --- Generate schema ---
|
|
278
|
+
|
|
279
|
+
s.start("Generating CMS schema");
|
|
280
|
+
try {
|
|
281
|
+
execSync(`${pm.run} cms:generate`, { cwd: projectDir, stdio: "pipe" });
|
|
282
|
+
s.stop("Schema generated");
|
|
283
|
+
} catch {
|
|
284
|
+
s.stop("Schema generation failed — run `cms:generate` manually");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// --- Seed demo content ---
|
|
288
|
+
|
|
289
|
+
if (seedDemo && target === "local") {
|
|
152
290
|
s.start("Pushing schema to database");
|
|
291
|
+
// Ensure data/ directory exists — drizzle-kit push silently exits 0 if it can't open the file
|
|
292
|
+
mkdirSync(path.join(projectDir, "data"), { recursive: true });
|
|
293
|
+
let pushOk = false;
|
|
153
294
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
295
|
+
const out = execSync(`${pm.exec} drizzle-kit push --force`, {
|
|
296
|
+
cwd: projectDir,
|
|
297
|
+
stdio: "pipe",
|
|
298
|
+
}).toString();
|
|
299
|
+
pushOk = !out.includes("Error:") && out.includes("Changes applied");
|
|
300
|
+
s.stop(pushOk ? "Schema pushed" : "Schema push failed — run `pnpm exec drizzle-kit push` manually");
|
|
156
301
|
} catch {
|
|
157
|
-
s.stop("Schema
|
|
302
|
+
s.stop("Schema will be set up on first dev start");
|
|
158
303
|
}
|
|
159
|
-
|
|
160
304
|
s.start("Seeding demo content");
|
|
161
305
|
try {
|
|
162
|
-
|
|
306
|
+
execSync(`${pm.run} cms:seed`, { cwd: projectDir, stdio: "pipe" });
|
|
163
307
|
s.stop("Demo content seeded");
|
|
164
|
-
} catch {
|
|
308
|
+
} catch (err) {
|
|
165
309
|
s.stop("Seeding failed — run `pnpm cms:seed` manually");
|
|
310
|
+
if (err.stderr) console.error(err.stderr.toString());
|
|
311
|
+
if (err.stdout) console.error(err.stdout.toString());
|
|
166
312
|
}
|
|
313
|
+
} else if (seedDemo && target === "cloudflare") {
|
|
314
|
+
p.note(
|
|
315
|
+
[
|
|
316
|
+
"Seeding for Cloudflare requires a D1 database.",
|
|
317
|
+
"",
|
|
318
|
+
` ${pm.dlx} wrangler d1 create ${projectName}-db`,
|
|
319
|
+
" # Add the database_id to wrangler.toml",
|
|
320
|
+
` ${pm.dlx} wrangler d1 migrations apply ${projectName}-db --local`,
|
|
321
|
+
` ${pm.run} cms:seed`,
|
|
322
|
+
].join("\n"),
|
|
323
|
+
"Seed manually",
|
|
324
|
+
);
|
|
167
325
|
}
|
|
168
326
|
|
|
169
327
|
// --- Cloudflare resource setup ---
|
|
170
328
|
|
|
171
|
-
const cf = { d1Created: false, r2Created: false, migrationsApplied: false };
|
|
172
|
-
if (cloudflare) {
|
|
329
|
+
const cf = { d1Created: false, r2Created: false, migrationsApplied: false, deployed: false, url: null };
|
|
330
|
+
if (target === "cloudflare") {
|
|
173
331
|
const setupNow = await p.confirm({
|
|
174
332
|
message: "Set up Cloudflare resources now? (creates D1 database and R2 bucket)",
|
|
175
333
|
initialValue: true,
|
|
@@ -179,17 +337,17 @@ async function main() {
|
|
|
179
337
|
// Check wrangler authentication
|
|
180
338
|
let authenticated = false;
|
|
181
339
|
try {
|
|
182
|
-
|
|
340
|
+
execSync(`${pm.exec} wrangler whoami`, { cwd: projectDir, stdio: "pipe" });
|
|
183
341
|
authenticated = true;
|
|
184
342
|
} catch {
|
|
185
343
|
p.note("You need to log in to Cloudflare first.", "Wrangler login required");
|
|
186
344
|
const doLogin = await p.confirm({ message: "Open browser to log in?", initialValue: true });
|
|
187
345
|
if (!p.isCancel(doLogin) && doLogin) {
|
|
188
346
|
try {
|
|
189
|
-
execSync(
|
|
347
|
+
execSync(`${pm.exec} wrangler login`, { cwd: projectDir, stdio: "inherit" });
|
|
190
348
|
authenticated = true;
|
|
191
349
|
} catch {
|
|
192
|
-
|
|
350
|
+
s.stop("Login failed");
|
|
193
351
|
}
|
|
194
352
|
}
|
|
195
353
|
}
|
|
@@ -199,16 +357,20 @@ async function main() {
|
|
|
199
357
|
let databaseId = null;
|
|
200
358
|
s.start("Creating D1 database");
|
|
201
359
|
try {
|
|
202
|
-
const
|
|
203
|
-
|
|
360
|
+
const output = execSync(`${pm.exec} wrangler d1 create ${projectName}-db`, {
|
|
361
|
+
cwd: projectDir,
|
|
362
|
+
stdio: "pipe",
|
|
363
|
+
}).toString();
|
|
364
|
+
const match = output.match(/database_id\s*=\s*"([^"]+)"/);
|
|
204
365
|
if (match) databaseId = match[1];
|
|
205
366
|
cf.d1Created = true;
|
|
206
367
|
s.stop("D1 database created");
|
|
207
|
-
} catch {
|
|
368
|
+
} catch (err) {
|
|
208
369
|
// Already exists — look it up
|
|
209
370
|
try {
|
|
210
|
-
const
|
|
211
|
-
const
|
|
371
|
+
const listOutput = execSync(`${pm.exec} wrangler d1 list`, { cwd: projectDir, stdio: "pipe" }).toString();
|
|
372
|
+
const lines = listOutput.split("\n");
|
|
373
|
+
const dbLine = lines.find((l) => l.includes(`${projectName}-db`));
|
|
212
374
|
if (dbLine) {
|
|
213
375
|
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
376
|
if (idMatch) databaseId = idMatch[0];
|
|
@@ -218,6 +380,7 @@ async function main() {
|
|
|
218
380
|
s.stop("D1 database already exists — using existing");
|
|
219
381
|
} else {
|
|
220
382
|
s.stop("D1 setup failed");
|
|
383
|
+
if (err.stderr) console.error(err.stderr.toString());
|
|
221
384
|
}
|
|
222
385
|
} catch {
|
|
223
386
|
s.stop("D1 setup failed");
|
|
@@ -228,87 +391,122 @@ async function main() {
|
|
|
228
391
|
if (databaseId) {
|
|
229
392
|
const wranglerPath = path.join(projectDir, "wrangler.toml");
|
|
230
393
|
let wranglerContent = readFileSync(wranglerPath, "utf-8");
|
|
231
|
-
wranglerContent = wranglerContent.replace(
|
|
232
|
-
/database_id = "" #[^\n]*/,
|
|
233
|
-
`database_id = "${databaseId}"`,
|
|
234
|
-
);
|
|
394
|
+
wranglerContent = wranglerContent.replace(/database_id = "" #[^\n]*/, `database_id = "${databaseId}"`);
|
|
235
395
|
writeFileSync(wranglerPath, wranglerContent);
|
|
236
396
|
}
|
|
237
397
|
|
|
238
398
|
// Create R2 bucket
|
|
239
399
|
s.start("Creating R2 bucket");
|
|
240
400
|
try {
|
|
241
|
-
|
|
401
|
+
execSync(`${pm.exec} wrangler r2 bucket create ${projectName}-assets`, { cwd: projectDir, stdio: "pipe" });
|
|
242
402
|
cf.r2Created = true;
|
|
243
403
|
s.stop("R2 bucket created");
|
|
244
404
|
} catch {
|
|
245
|
-
// Already exists is fine
|
|
246
405
|
cf.r2Created = true;
|
|
247
406
|
s.stop("R2 bucket already exists");
|
|
248
407
|
}
|
|
249
408
|
|
|
250
|
-
// Generate
|
|
409
|
+
// Generate migrations and apply to remote D1
|
|
251
410
|
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
411
|
s.start("Generating database migrations");
|
|
261
412
|
try {
|
|
262
|
-
|
|
413
|
+
execSync(`${pm.exec} drizzle-kit generate`, { cwd: projectDir, stdio: "pipe" });
|
|
263
414
|
s.stop("Migrations generated");
|
|
264
|
-
} catch {
|
|
415
|
+
} catch (err) {
|
|
265
416
|
s.stop("Migration generation failed");
|
|
417
|
+
if (err.stderr) console.error(err.stderr.toString().slice(-800));
|
|
418
|
+
if (err.stdout) console.error(err.stdout.toString().slice(-800));
|
|
266
419
|
}
|
|
267
420
|
|
|
268
421
|
s.start("Applying migrations to remote D1");
|
|
269
422
|
try {
|
|
270
|
-
|
|
423
|
+
execSync(`${pm.exec} wrangler d1 migrations apply ${projectName}-db --remote`, {
|
|
424
|
+
cwd: projectDir,
|
|
425
|
+
stdio: "pipe",
|
|
426
|
+
input: "y\n",
|
|
427
|
+
});
|
|
271
428
|
cf.migrationsApplied = true;
|
|
272
429
|
s.stop("Migrations applied");
|
|
273
430
|
} catch {
|
|
274
431
|
s.stop("Migration apply failed — run manually with: wrangler d1 migrations apply --remote");
|
|
275
432
|
}
|
|
276
433
|
}
|
|
434
|
+
|
|
435
|
+
// Deploy to Cloudflare
|
|
436
|
+
if (cf.migrationsApplied) {
|
|
437
|
+
const doDeploy = await p.confirm({
|
|
438
|
+
message: "Deploy to Cloudflare now?",
|
|
439
|
+
initialValue: true,
|
|
440
|
+
});
|
|
441
|
+
if (!p.isCancel(doDeploy) && doDeploy) {
|
|
442
|
+
s.start("Building and deploying to Cloudflare");
|
|
443
|
+
try {
|
|
444
|
+
const deployOutput = await runAsync(`${pm.run} run deploy`, projectDir);
|
|
445
|
+
const urlMatch = deployOutput.match(/https:\/\/[^\s]+\.workers\.dev/);
|
|
446
|
+
if (urlMatch) cf.url = urlMatch[0];
|
|
447
|
+
cf.deployed = true;
|
|
448
|
+
s.stop("Deployed to Cloudflare");
|
|
449
|
+
} catch (err) {
|
|
450
|
+
s.stop("Deploy failed — run manually with: pnpm run deploy");
|
|
451
|
+
if (err.stderr) console.error(err.stderr.slice(-1500));
|
|
452
|
+
if (err.stdout) console.error(err.stdout.slice(-1500));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
277
456
|
}
|
|
278
457
|
}
|
|
279
458
|
}
|
|
280
459
|
|
|
281
460
|
// --- Done ---
|
|
282
461
|
|
|
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 {
|
|
462
|
+
if (target === "local") {
|
|
305
463
|
p.outro("Starting dev server...");
|
|
306
464
|
try {
|
|
307
|
-
execSync(
|
|
465
|
+
execSync(`${pm.run} dev`, { cwd: projectDir, stdio: "inherit" });
|
|
308
466
|
} catch {
|
|
309
467
|
console.log(`\n Project directory: ${projectDir}`);
|
|
310
468
|
console.log(` To start again: cd ${projectName} && pnpm dev\n`);
|
|
311
469
|
}
|
|
470
|
+
} else {
|
|
471
|
+
if (cf.deployed && cf.url) {
|
|
472
|
+
p.note(
|
|
473
|
+
[
|
|
474
|
+
`Live at: ${cf.url}`,
|
|
475
|
+
`Admin: ${cf.url}/admin`,
|
|
476
|
+
"",
|
|
477
|
+
`cd ${projectName}`,
|
|
478
|
+
"",
|
|
479
|
+
"Local development:",
|
|
480
|
+
` ${pm.run} dev`,
|
|
481
|
+
"",
|
|
482
|
+
"Redeploy:",
|
|
483
|
+
" pnpm run deploy",
|
|
484
|
+
].join("\n"),
|
|
485
|
+
"🎉 Your Kide CMS is live",
|
|
486
|
+
);
|
|
487
|
+
p.outro("Project created!");
|
|
488
|
+
} else {
|
|
489
|
+
const lines = [`cd ${projectName}`];
|
|
490
|
+
const remaining = [];
|
|
491
|
+
if (!cf.d1Created) {
|
|
492
|
+
remaining.push(` ${pm.dlx} wrangler d1 create ${projectName}-db`, " # Copy the database_id to wrangler.toml");
|
|
493
|
+
}
|
|
494
|
+
if (!cf.r2Created) {
|
|
495
|
+
remaining.push(` ${pm.dlx} wrangler r2 bucket create ${projectName}-assets`);
|
|
496
|
+
}
|
|
497
|
+
if (!cf.migrationsApplied) {
|
|
498
|
+
remaining.push(` ${pm.dlx} wrangler d1 migrations apply ${projectName}-db --remote`);
|
|
499
|
+
}
|
|
500
|
+
if (!cf.deployed) {
|
|
501
|
+
remaining.push(` ${pm.run} run deploy`);
|
|
502
|
+
}
|
|
503
|
+
if (remaining.length > 0) {
|
|
504
|
+
lines.push("", "Remaining setup:", ...remaining);
|
|
505
|
+
}
|
|
506
|
+
lines.push("", "Local development:", ` ${pm.run} dev`);
|
|
507
|
+
p.note(lines.join("\n"), "Next steps");
|
|
508
|
+
p.outro("Project created!");
|
|
509
|
+
}
|
|
312
510
|
}
|
|
313
511
|
}
|
|
314
512
|
|