create-kide-app 0.1.2 → 0.1.4

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