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.
Files changed (3) hide show
  1. package/README.md +11 -12
  2. package/index.js +319 -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,233 @@ 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} --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
- await run("pnpm exec drizzle-kit push --force", projectDir);
155
- s.stop("Schema pushed");
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 push failed — will be set up on first dev start");
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
- await run("pnpm cms:seed", projectDir);
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
- await run("pnpm wrangler whoami", projectDir);
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("pnpm wrangler login", { cwd: projectDir, stdio: "inherit" });
339
+ execSync(`${pm.exec} wrangler login`, { cwd: projectDir, stdio: "inherit" });
190
340
  authenticated = true;
191
341
  } catch {
192
- // Login failed
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 { stdout } = await run(`pnpm wrangler d1 create ${projectName}-db`, projectDir);
203
- const match = stdout.match(/database_id\s*=\s*"([^"]+)"/);
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 { stdout } = await run("pnpm wrangler d1 list", projectDir);
211
- const dbLine = stdout.split("\n").find((l) => l.includes(`${projectName}-db`));
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
- await run(`pnpm wrangler r2 bucket create ${projectName}-assets`, projectDir);
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 schema + migrations, apply to remote D1
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
- await run("pnpm exec drizzle-kit generate", projectDir);
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
- await run(`pnpm wrangler d1 migrations apply ${projectName}-db --remote`, projectDir);
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 (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 {
454
+ if (target === "local") {
305
455
  p.outro("Starting dev server...");
306
456
  try {
307
- execSync("pnpm dev", { cwd: projectDir, stdio: "inherit" });
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
 
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.4",
4
4
  "description": "Scaffold a new Kide CMS project",
5
5
  "author": "Matti Hernesniemi",
6
6
  "license": "MIT",