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.
Files changed (3) hide show
  1. package/README.md +11 -12
  2. package/index.js +327 -129
  3. 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
- npm create kide-app my-project
12
+ bunx create-kide-app my-project
11
13
  ```
12
14
 
13
- The CLI asks for:
15
+ The CLI will guide you through:
14
16
 
15
17
  1. **Project name** — directory to create
16
- 2. **Deploy target** — Node.js (default) or Cloudflare
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
- 1. Clones the latest `kide-cms` from GitHub (shallow).
21
- 2. Removes the embedded `.git` and renames the project in `package.json`.
22
- 3. If Cloudflare is selected, overlays the files from `adapters/cloudflare/` and patches `package.json` (swaps `@astrojs/node` → `@astrojs/cloudflare`, adds `wrangler`, removes `sharp`, etc.).
23
- 4. Removes the `adapters/` scaffolding directory.
24
- 5. Runs `pnpm install`.
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 { exec as execCb, 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
- import { promisify } from "node:util";
8
7
 
9
- const exec = promisify(execCb);
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
- function run(cmd, cwd) {
13
- return exec(cmd, { cwd });
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
- function applyPackagePatch(pkg, patch) {
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) => (value ? undefined : "Project name is required"),
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
- const cloudflare = await p.confirm({
61
- message: "Deploy to Cloudflare? (otherwise Node.js)",
62
- initialValue: false,
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(cloudflare)) {
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 (!cloudflare) {
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
- // --- Clone kide-cms ---
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
- s.start("Cloning kide-cms");
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
- // --- Adjust package.json and apply adapter overlay ---
118
+ s.start(`Applying ${target} configuration`);
93
119
 
94
- s.start(cloudflare ? "Applying Cloudflare adapter" : "Configuring project");
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
- const adaptersDir = path.join(projectDir, "adapters");
138
+ if (target === "cloudflare") {
139
+ delete pkg.dependencies["@astrojs/node"];
140
+ pkg.dependencies["@astrojs/cloudflare"] = "^13.0.0";
102
141
 
103
- if (cloudflare) {
104
- const cfDir = path.join(adaptersDir, "cloudflare");
105
- const overlayFiles = [
106
- "astro.config.mjs",
107
- "drizzle.config.ts",
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
- const wrangler = readFileSync(path.join(cfDir, "wrangler.toml"), "utf-8").replaceAll(
117
- "{{PROJECT_NAME}}",
118
- projectName,
119
- );
120
- 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);
153
+
154
+ pkg.devDependencies.wrangler = "^4.0.0";
121
155
 
122
- const patch = JSON.parse(readFileSync(path.join(cfDir, "package.patch.json"), "utf-8"));
123
- applyPackagePatch(pkg, patch);
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
- s.stop(cloudflare ? "Cloudflare adapter applied" : "Project configured");
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 run("pnpm install", projectDir);
172
+ await runAsync(pm.install, projectDir);
136
173
  s.stop("Dependencies installed");
137
174
  } catch {
138
- s.stop("pnpm install failed — run it manually");
175
+ s.stop(`${pm.install} failed — run it manually`);
139
176
  }
140
177
 
141
- // --- Seed demo content (local only) ---
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
- if (seedDemo) {
144
- s.start("Generating CMS schema");
191
+ // --- Optional: create GitHub repository ---
192
+
193
+ if (gitInitialized) {
194
+ let ghAvailable = false;
145
195
  try {
146
- await run("pnpm cms:generate", projectDir);
147
- s.stop("Schema generated");
196
+ execSync("gh --version", { stdio: "pipe" });
197
+ execSync("gh auth status", { stdio: "pipe" });
198
+ ghAvailable = true;
148
199
  } catch {
149
- s.stop("Schema generation failedrun `pnpm cms:generate` manually");
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
- await run("pnpm exec drizzle-kit push --force", projectDir);
155
- s.stop("Schema pushed");
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 push failed — will be set up on first dev start");
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
- await run("pnpm cms:seed", projectDir);
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
- await run("pnpm wrangler whoami", projectDir);
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("pnpm wrangler login", { cwd: projectDir, stdio: "inherit" });
347
+ execSync(`${pm.exec} wrangler login`, { cwd: projectDir, stdio: "inherit" });
190
348
  authenticated = true;
191
349
  } catch {
192
- // Login failed
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 { stdout } = await run(`pnpm wrangler d1 create ${projectName}-db`, projectDir);
203
- const match = stdout.match(/database_id\s*=\s*"([^"]+)"/);
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 { stdout } = await run("pnpm wrangler d1 list", projectDir);
211
- const dbLine = stdout.split("\n").find((l) => l.includes(`${projectName}-db`));
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
- await run(`pnpm wrangler r2 bucket create ${projectName}-assets`, projectDir);
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 schema + migrations, apply to remote D1
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
- await run("pnpm exec drizzle-kit generate", projectDir);
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
- await run(`pnpm wrangler d1 migrations apply ${projectName}-db --remote`, projectDir);
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 (cloudflare) {
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("pnpm dev", { cwd: projectDir, stdio: "inherit" });
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kide-app",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Scaffold a new Kide CMS project",
5
5
  "author": "Matti Hernesniemi",
6
6
  "license": "MIT",