create-kide-app 0.2.2 → 0.3.0

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 +22 -3
  2. package/index.js +222 -65
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -11,10 +11,29 @@ pnpm create kide-app my-project
11
11
  The CLI asks for:
12
12
 
13
13
  1. **Project name** — directory to create
14
- 2. **Distribution mode** — Package (recommended: thin project + `@kidecms/core` npm dependency; `pnpm exec kide eject` converts to embedded later, one-way) or Embedded (full CMS source in `src/cms/`, yours to modify)
15
- 3. **Deploy target** — Local/Node.js or Cloudflare
14
+ 2. **Starter template** — Blank (default: one `pages` collection, no demo content) or a starter shipped with the template release, e.g. Marketing site (pages with blocks, blog, menu, contact form). Starters can seed example content into the local database.
15
+ 3. **Distribution mode** — Package (recommended: thin project + `@kidecms/core` npm dependency; `pnpm exec kide eject` converts to embedded later, one-way) or Embedded (full CMS source in `src/cms/`, yours to modify)
16
+ 4. **Deploy target** — Local/Node.js or Cloudflare
16
17
 
17
- Every project starts bare-bones: one `pages` collection (`title`, `slug`, `body`) and a minimal public page — no demo content, no seeds.
18
+ ## Non-interactive use
19
+
20
+ Every prompt can be answered with a flag; supplied answers skip their prompts.
21
+
22
+ ```bash
23
+ pnpm create kide-app my-app --starter=marketing --seed --mode=embedded --target=local --no-github --no-dev
24
+ ```
25
+
26
+ | Flag | Values |
27
+ | ---- | ------ |
28
+ | `--starter=` | `blank` or a starter name from the template release |
29
+ | `--seed` / `--no-seed` | Seed example content (local target only) |
30
+ | `--mode=` | `package` or `embedded` |
31
+ | `--target=` | `local` or `cloudflare` |
32
+ | `--no-github` | Skip the GitHub repo prompt |
33
+ | `--no-dev` | Skip the dev-server prompt |
34
+ | `--no-cloudflare-setup` | Skip Cloudflare resource provisioning |
35
+
36
+ The template repo can be overridden with the `KIDE_TEMPLATE_REPO` env var (forks, local testing).
18
37
 
19
38
  ## What it does
20
39
 
package/index.js CHANGED
@@ -5,6 +5,7 @@ import { execFileSync, execSync, spawn } from "node:child_process";
5
5
  import {
6
6
  cpSync,
7
7
  existsSync,
8
+ readdirSync,
8
9
  readFileSync,
9
10
  rmSync,
10
11
  writeFileSync,
@@ -42,7 +43,11 @@ const pm = {
42
43
 
43
44
  // --- Template repo ---
44
45
 
45
- const REPO = "https://github.com/mhernesniemi/kide-cms.git";
46
+ // Overridable for forks and for testing against a local checkout
47
+ // (e.g. KIDE_TEMPLATE_REPO=file:///path/to/kide-cms).
48
+ const REPO =
49
+ process.env.KIDE_TEMPLATE_REPO ||
50
+ "https://github.com/mhernesniemi/kide-cms.git";
46
51
 
47
52
  // Files from the kide-cms repo that shouldn't leak into scaffolded projects.
48
53
  // NOTE: `.claude/settings.local.json` is removed but `.claude/skills/` is kept,
@@ -106,14 +111,50 @@ const resolveLatestTag = () => {
106
111
  return null;
107
112
  };
108
113
 
114
+ // --- CLI flags (non-interactive use: CI, agents, testing) ---
115
+ // Any prompt whose answer is supplied by a flag is skipped. Example:
116
+ // create-kide-app my-app --starter=marketing --seed --mode=embedded --target=local --no-github --no-dev
117
+
118
+ const parseArgs = (argv) => {
119
+ const flags = {};
120
+ const positional = [];
121
+ for (const arg of argv) {
122
+ if (arg === "--seed") flags.seed = true;
123
+ else if (arg === "--no-seed") flags.seed = false;
124
+ else if (arg === "--no-github") flags.noGithub = true;
125
+ else if (arg === "--no-dev") flags.noDev = true;
126
+ else if (arg === "--no-cloudflare-setup") flags.noCloudflareSetup = true;
127
+ else if (arg.startsWith("--starter=")) flags.starter = arg.slice("--starter=".length);
128
+ else if (arg.startsWith("--mode=")) flags.mode = arg.slice("--mode=".length);
129
+ else if (arg.startsWith("--target=")) flags.target = arg.slice("--target=".length);
130
+ else if (arg.startsWith("--")) flags.unknown = arg;
131
+ else positional.push(arg);
132
+ }
133
+ return { flags, positional };
134
+ };
135
+
109
136
  // --- Main ---
110
137
 
111
138
  async function main() {
112
139
  p.intro("🪐 Create Kide CMS Project");
113
140
 
141
+ const { flags, positional } = parseArgs(process.argv.slice(2));
142
+ if (flags.unknown) {
143
+ p.cancel(`Unknown flag: ${flags.unknown}`);
144
+ process.exit(1);
145
+ }
146
+ if (flags.mode !== undefined && !["package", "embedded"].includes(flags.mode)) {
147
+ p.cancel(`--mode must be "package" or "embedded".`);
148
+ process.exit(1);
149
+ }
150
+ if (flags.target !== undefined && !["local", "cloudflare"].includes(flags.target)) {
151
+ p.cancel(`--target must be "local" or "cloudflare".`);
152
+ process.exit(1);
153
+ }
154
+
114
155
  // 1. Project name
115
156
  const projectName =
116
- process.argv[2] ||
157
+ positional[0] ||
117
158
  (await p.text({
118
159
  message: "Project name",
119
160
  placeholder: "my-cms-app",
@@ -142,48 +183,14 @@ async function main() {
142
183
  process.exit(1);
143
184
  }
144
185
 
145
- // 2. Distribution mode — package is the recommended default; embedded stays
146
- // first-class for teams that want to own and modify the runtime source.
147
- const mode = await p.select({
148
- message: "How do you want the CMS runtime?",
149
- options: [
150
- {
151
- label: "Package (recommended)",
152
- value: "package",
153
- hint: "thin project + @kidecms/core dependency — most updates are a version bump; eject to embedded later",
154
- },
155
- {
156
- label: "Embedded",
157
- value: "embedded",
158
- hint: "full CMS source in src/cms/ — modify internals, audit everything; upgrades arrive as release packets",
159
- },
160
- ],
161
- });
162
-
163
- if (p.isCancel(mode)) {
164
- p.cancel("Setup cancelled.");
165
- process.exit(0);
166
- }
167
-
168
- // 3. Deploy target
169
- const target = await p.select({
170
- message: "Where will you deploy?",
171
- options: [
172
- { label: "Local / Node.js", value: "local" },
173
- { label: "Cloudflare", value: "cloudflare" },
174
- ],
175
- });
176
-
177
- if (p.isCancel(target)) {
178
- p.cancel("Setup cancelled.");
179
- process.exit(0);
180
- }
181
-
182
186
  const s = p.spinner();
183
187
 
184
188
  // --- Scaffold via git clone ---
189
+ // The clone happens before the remaining prompts so the starter list can be
190
+ // read from the cloned tag — new starters ship with template releases, no CLI
191
+ // release needed.
185
192
 
186
- s.start(`Scaffolding project (using ${pm.name})`);
193
+ s.start("Downloading template");
187
194
 
188
195
  const templateRef = resolveLatestTag();
189
196
  let templateCommit = null;
@@ -211,6 +218,7 @@ async function main() {
211
218
  }
212
219
  rmSync(path.join(projectDir, ".git"), { recursive: true, force: true });
213
220
  } catch {
221
+ rmSync(projectDir, { recursive: true, force: true });
214
222
  s.stop("Failed to download template.");
215
223
  p.cancel("Check your network connection.");
216
224
  process.exit(1);
@@ -221,6 +229,127 @@ async function main() {
221
229
  rmSync(path.join(projectDir, f), { recursive: true, force: true });
222
230
  }
223
231
 
232
+ s.stop(templateRef ? `Template ready (${templateRef})` : "Template ready");
233
+
234
+ // From here on the clone exists, so a cancelled prompt must remove it.
235
+ const cancelSetup = (message = "Setup cancelled.") => {
236
+ rmSync(projectDir, { recursive: true, force: true });
237
+ p.cancel(message);
238
+ process.exit(0);
239
+ };
240
+
241
+ // 2. Starter template — options come from starters/*/starter.json in the
242
+ // cloned tag. Older tags have no starters/ dir; the prompt is skipped and the
243
+ // scaffold stays blank.
244
+ const startersDir = path.join(projectDir, "starters");
245
+ const starterOptions = [];
246
+ if (existsSync(startersDir)) {
247
+ for (const entry of readdirSync(startersDir, { withFileTypes: true })) {
248
+ if (!entry.isDirectory()) continue;
249
+ const manifestPath = path.join(startersDir, entry.name, "starter.json");
250
+ if (!existsSync(manifestPath)) continue;
251
+ try {
252
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
253
+ starterOptions.push({
254
+ label: manifest.label ?? entry.name,
255
+ value: entry.name,
256
+ hint: manifest.hint,
257
+ order: manifest.order ?? 100,
258
+ });
259
+ } catch {
260
+ // unreadable manifest — skip this starter
261
+ }
262
+ }
263
+ starterOptions.sort((a, b) => a.order - b.order);
264
+ }
265
+
266
+ let starter = null;
267
+ if (flags.starter !== undefined) {
268
+ if (flags.starter !== "blank") {
269
+ if (!starterOptions.some((option) => option.value === flags.starter)) {
270
+ cancelSetup(
271
+ `Unknown starter "${flags.starter}". Available: blank${starterOptions.map((o) => `, ${o.value}`).join("")}`,
272
+ );
273
+ }
274
+ starter = flags.starter;
275
+ }
276
+ } else if (starterOptions.length > 0) {
277
+ const choice = await p.select({
278
+ message: "Starter template",
279
+ options: [
280
+ { label: "Blank", value: null, hint: "empty schema, no demo content" },
281
+ ...starterOptions.map(({ label, value, hint }) => ({
282
+ label,
283
+ value,
284
+ hint,
285
+ })),
286
+ ],
287
+ });
288
+ if (p.isCancel(choice)) cancelSetup();
289
+ starter = choice;
290
+ }
291
+
292
+ // 3. Distribution mode — package is the recommended default; embedded stays
293
+ // first-class for teams that want to own and modify the runtime source.
294
+ const mode =
295
+ flags.mode ??
296
+ (await p.select({
297
+ message: "How do you want the CMS runtime?",
298
+ options: [
299
+ {
300
+ label: "Package",
301
+ value: "package",
302
+ hint: "@kidecms/core dependency",
303
+ },
304
+ {
305
+ label: "Embedded",
306
+ value: "embedded",
307
+ hint: "full CMS source in src/cms/, yours to modify",
308
+ },
309
+ ],
310
+ }));
311
+
312
+ if (p.isCancel(mode)) cancelSetup();
313
+
314
+ // 4. Deploy target
315
+ const target =
316
+ flags.target ??
317
+ (await p.select({
318
+ message: "Where will you deploy?",
319
+ options: [
320
+ { label: "Local / Node.js", value: "local" },
321
+ { label: "Cloudflare", value: "cloudflare" },
322
+ ],
323
+ }));
324
+
325
+ if (p.isCancel(target)) cancelSetup();
326
+
327
+ // Seeding runs at scaffold time and needs the local db adapter — the
328
+ // Cloudflare adapter needs Worker bindings, so the question is local-only.
329
+ let seedRequested = false;
330
+ if (starter && target === "local") {
331
+ if (flags.seed !== undefined) {
332
+ seedRequested = flags.seed;
333
+ } else {
334
+ const seedChoice = await p.confirm({
335
+ message: "Seed example content?",
336
+ initialValue: true,
337
+ });
338
+ if (p.isCancel(seedChoice)) cancelSetup();
339
+ seedRequested = seedChoice;
340
+ }
341
+ }
342
+
343
+ s.start(`Configuring project (using ${pm.name})`);
344
+
345
+ // Apply the starter overlay — project-owned files copied over the barebone
346
+ // base. starter.json is manifest metadata, not project content.
347
+ if (starter) {
348
+ cpSync(path.join(startersDir, starter), projectDir, { recursive: true });
349
+ rmSync(path.join(projectDir, "starter.json"), { force: true });
350
+ }
351
+ rmSync(startersDir, { recursive: true, force: true });
352
+
224
353
  // Both modes scaffold the same template at the same tag; package mode then
225
354
  // deletes the managed runtime dirs and swaps the workspace link for the
226
355
  // published @kidecms/core at exactly that version — same source either way.
@@ -317,6 +446,15 @@ async function main() {
317
446
  delete pkg.scripts["check:cloudflare"];
318
447
  pkg.scripts.check = "astro check && eslint .";
319
448
  pkg.scripts.test = "pnpm cms:generate && vitest run --passWithNoTests";
449
+ // Upstream test-fixture tooling lives in the deleted core/__tests__ tree.
450
+ delete pkg.scripts["test:fixtures"];
451
+ rmSync(path.join(projectDir, "scripts/generate-test-fixtures.ts"), {
452
+ force: true,
453
+ });
454
+ // Dev tooling for the deleted worker tests / Cloudflare type profile.
455
+ delete pkg.devDependencies["@cloudflare/vitest-pool-workers"];
456
+ delete pkg.devDependencies["@cloudflare/workers-types"];
457
+ delete pkg.devDependencies["jsdom"];
320
458
  }
321
459
 
322
460
  if (target === "cloudflare") {
@@ -395,6 +533,7 @@ async function main() {
395
533
  commit: templateCommit,
396
534
  target,
397
535
  mode,
536
+ starter: starter ?? null,
398
537
  corePath: "src/cms",
399
538
  scaffoldedAt: new Date().toISOString(),
400
539
  createKideApp: cliVersion,
@@ -462,7 +601,7 @@ async function main() {
462
601
  // gh not installed or not authenticated — skip the prompt
463
602
  }
464
603
 
465
- if (ghAvailable) {
604
+ if (ghAvailable && !flags.noGithub) {
466
605
  const createRepo = await p.confirm({
467
606
  message: "Create a GitHub repository for this project?",
468
607
  initialValue: false,
@@ -558,6 +697,21 @@ async function main() {
558
697
  s.stop("Schema generation failed — run `cms:generate` manually");
559
698
  }
560
699
 
700
+ // --- Seed starter content (local target only) ---
701
+
702
+ if (starter && seedRequested && target === "local") {
703
+ s.start("Seeding example content");
704
+ try {
705
+ await runAsync(`${pm.run} cms:push`, projectDir);
706
+ await runAsync(`${pm.exec} kide seed`, projectDir);
707
+ s.stop("Example content seeded");
708
+ } catch {
709
+ s.stop(
710
+ "Seeding failed — run `pnpm cms:push && pnpm cms:seed` manually",
711
+ );
712
+ }
713
+ }
714
+
561
715
  // --- Cloudflare resource setup ---
562
716
 
563
717
  const cf = {
@@ -568,11 +722,14 @@ async function main() {
568
722
  url: null,
569
723
  };
570
724
  if (target === "cloudflare") {
571
- const setupNow = await p.confirm({
572
- message:
573
- "Set up Cloudflare resources now? (creates D1 database and R2 bucket)",
574
- initialValue: true,
575
- });
725
+ const setupNow =
726
+ flags.noCloudflareSetup === true
727
+ ? false
728
+ : await p.confirm({
729
+ message:
730
+ "Set up Cloudflare resources now? (creates D1 database and R2 bucket)",
731
+ initialValue: true,
732
+ });
576
733
 
577
734
  if (!p.isCancel(setupNow) && setupNow) {
578
735
  // Check wrangler authentication
@@ -748,10 +905,12 @@ async function main() {
748
905
  // --- Done ---
749
906
 
750
907
  if (target === "local") {
751
- const startDev = await p.confirm({
752
- message: "Start the dev server now?",
753
- initialValue: true,
754
- });
908
+ const startDev = flags.noDev
909
+ ? false
910
+ : await p.confirm({
911
+ message: "Start the dev server now?",
912
+ initialValue: true,
913
+ });
755
914
 
756
915
  if (!p.isCancel(startDev) && startDev) {
757
916
  p.outro("Starting dev server...");
@@ -770,21 +929,19 @@ async function main() {
770
929
  }
771
930
  } else {
772
931
  if (cf.deployed && cf.url) {
773
- p.note(
774
- [
775
- `Live at: ${cf.url}`,
776
- `Admin: ${cf.url}/admin`,
777
- "",
778
- `cd ${projectName}`,
779
- "",
780
- "Local development:",
781
- ` ${pm.run} dev`,
782
- "",
783
- "Redeploy:",
784
- " pnpm run deploy",
785
- ].join("\n"),
786
- "🎉 Your Kide CMS is live",
787
- );
932
+ const liveLines = [
933
+ `Live at: ${cf.url}`,
934
+ `Admin: ${cf.url}/admin`,
935
+ "",
936
+ `cd ${projectName}`,
937
+ "",
938
+ "Local development:",
939
+ ` ${pm.run} dev`,
940
+ "",
941
+ "Redeploy:",
942
+ " pnpm run deploy",
943
+ ];
944
+ p.note(liveLines.join("\n"), "🎉 Your Kide CMS is live");
788
945
  p.outro("Project created!");
789
946
  } else {
790
947
  const lines = [`cd ${projectName}`];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kide-app",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Scaffold a new Kide CMS project",
5
5
  "author": "Matti Hernesniemi",
6
6
  "license": "MIT",
@@ -9,7 +9,8 @@
9
9
  "create-kide-app": "./index.js"
10
10
  },
11
11
  "scripts": {
12
- "release": "npm version patch && git push --follow-tags && npm publish"
12
+ "release": "npm version patch && git push --follow-tags && npm publish",
13
+ "release:minor": "npm version minor && git push --follow-tags && npm publish"
13
14
  },
14
15
  "files": [
15
16
  "index.js"