create-catalyst-app 1.0.2-canary.1 → 1.0.2-canary.2

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/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "create-catalyst-app",
3
3
  "bin": "scripts/cli.cjs",
4
- "version": "1.0.2-canary.1",
4
+ "version": "1.0.2-canary.2",
5
+ "engines": {
6
+ "node": ">=20.12.0"
7
+ },
5
8
  "description": "cli package to scaffold Catalyst application",
6
9
  "repository": {
7
10
  "type": "git",
@@ -15,6 +18,7 @@
15
18
  "test:types": "tsc --noEmit"
16
19
  },
17
20
  "dependencies": {
21
+ "@clack/prompts": "^1.7.0",
18
22
  "ansis": "^4.3.1",
19
23
  "commander": "^8.2.0",
20
24
  "jscodeshift": "^0.16.0",
package/scripts/cli.cjs CHANGED
@@ -2,14 +2,25 @@
2
2
  const { execFileSync, execSync } = require("child_process")
3
3
  const Commander = require("commander")
4
4
  const { Option } = require("commander")
5
- const prompts = require("prompts")
6
- const { red, green, cyan, bold } = require("ansis")
5
+ // @clack/prompts is ESM-only. require() of ESM is only on by default from
6
+ // Node 20.19 / 22.12 (engines allows >=20.12), so it is imported lazily in the
7
+ // scaffold action -- the only path that uses it.
8
+ /** @type {typeof import("@clack/prompts")} */
9
+ let clack
10
+ const { red, green, cyan, gray, bold } = require("ansis")
7
11
  const tar = require("tar")
8
12
  const path = require("path")
9
13
  const fs = require("fs")
10
14
  var validate = require("validate-npm-package-name")
11
15
  const packageJson = require("../package.json")
12
- const { CCAError, createError, wrapForeignError, formatError, resolveOutputMode, getDebugEnvInfo } = require("./errors.cjs")
16
+ const {
17
+ CCAError,
18
+ createError,
19
+ wrapForeignError,
20
+ formatError,
21
+ resolveOutputMode,
22
+ getDebugEnvInfo,
23
+ } = require("./errors.cjs")
13
24
  const outputMode = resolveOutputMode(process.argv)
14
25
  // Colored (red) prefix only applies in default mode — verbose/debug boxes
15
26
  // own their coloring internally (see errors.cjs#box).
@@ -18,6 +29,15 @@ const printError = (err) => {
18
29
  const formatted = formatError(err, outputMode, debugEnv)
19
30
  console.error(outputMode === "default" ? red(formatted) : formatted)
20
31
  }
32
+ // Inside a clack session: close it with a one-line reason tagged with the
33
+ // error's code and its docs link, on stderr like every other error.
34
+ // --verbose/--debug follow it with the registry's full report, which carries
35
+ // the link itself.
36
+ const cancelWithError = (err, message = err.message) => {
37
+ const docs = outputMode === "default" && err.docUrl ? `\n ${gray(`Docs: ${err.docUrl}`)}` : ""
38
+ clack.cancel(`${message} ${gray(`[${err.code}]`)}${docs}`, { output: process.stderr })
39
+ if (outputMode !== "default") printError(err)
40
+ }
21
41
  const packageRoot = path.join(__dirname, "..")
22
42
  const executable = (command) => (process.platform === "win32" ? `${command}.cmd` : command)
23
43
 
@@ -54,15 +74,20 @@ const program = new Commander.Command()
54
74
  return
55
75
  }
56
76
 
57
- console.log(cyan(`Using create-catalyst-app version ${bold(packageJson.version)}`))
77
+ clack = await import("@clack/prompts")
78
+
79
+ // Same identity as every other Catalyst command: accent-coloured
80
+ // name, dim version.
81
+ clack.intro(`${cyan(bold("create-catalyst-app"))} ${gray(packageJson.version)}`)
58
82
 
59
83
  // Use options provided through commander or prompt the user
60
84
  validateOptions(cmd)
61
85
 
62
86
  if (cmd.yes) {
63
- console.log("Using default configuration.")
87
+ clack.log.info("Using default configuration")
64
88
  config = {
65
- folderName: "my-app",
89
+ // A name given on the command line still wins over the default.
90
+ folderName: folderName || "my-app",
66
91
  language: "js",
67
92
  tailWindSupport: false,
68
93
  description: "Default catalyst app",
@@ -72,30 +97,36 @@ const program = new Commander.Command()
72
97
  }
73
98
 
74
99
  const projectName = config.folderName || (await promptProjectName())
75
- let isNameValid = validate(projectName)
100
+
101
+ // Still needed for --yes and for a name passed as an argument:
102
+ // those paths never reach the prompt's own validation.
103
+ const isNameValid = validate(projectName)
76
104
  if (!isNameValid.validForNewPackages) {
77
- const reasons = [...(isNameValid?.warnings || []), ...(isNameValid?.errors || [])]
78
- printError(
79
- createError("CCA-001", {
80
- details: reasons.length ? reasons.join("; ") : undefined,
81
- })
105
+ const reasons = [...(isNameValid.errors || []), ...(isNameValid.warnings || [])]
106
+ cancelWithError(
107
+ createError("CCA-001", { details: reasons.length ? reasons.join("; ") : undefined }),
108
+ `"${projectName}" is not a valid package name: ${reasons.join(", ")}`
82
109
  )
83
110
  process.exit(1)
84
111
  }
85
112
  let projectPath = path.join(process.cwd(), projectName)
86
113
  if (fs.existsSync(projectPath)) {
87
- printError(
114
+ cancelWithError(
88
115
  createError("CCA-002", {
89
- message: `${projectName} already exists, try again.`,
116
+ message: `${projectName} already exists — choose another name or remove it.`,
90
117
  })
91
118
  )
92
119
  process.exit(1)
93
120
  }
94
121
  const projectDescription = config.description || (await promptDescription())
95
122
  const language = config.language || (await promptTypescript())
96
- const tailWindSupport = config.tailWindSupport !== null || (await promptTailwind())
123
+ // These are booleans, so `!== null` collapsed a configured `false`
124
+ // into `true` -- `--yes` installed Tailwind against its own default.
125
+ // Only fall through to the prompt when nothing was configured.
126
+ const tailWindSupport =
127
+ config.tailWindSupport != null ? config.tailWindSupport : await promptTailwind()
97
128
  const stateManagement = config.stateManagement || (await promptStateManagement())
98
- const mcpSupport = config.mcpSupport !== null || (await promptMcp())
129
+ const mcpSupport = config.mcpSupport != null ? config.mcpSupport : await promptMcp()
99
130
 
100
131
  // Define mapping of options to repository suffixes
101
132
  const repositorySuffixes = {
@@ -126,39 +157,30 @@ const program = new Commander.Command()
126
157
  extractSubdirectory(packageFilePath)
127
158
  createGitignore(projectName)
128
159
 
129
- execSync(
130
- `cd ${projectName} && npm i && npm pkg set name=${projectName} ${projectDescription ? `description="${projectDescription}"` : ""} && git init --quiet`,
131
- { stdio: "inherit" }
132
- )
160
+ // npm's install output is hundreds of lines nobody reads
161
+ // when it works. Fold it behind a spinner, and print it in
162
+ // full only if the step actually fails.
163
+ runStep(`Installing dependencies`, () => {
164
+ runQuiet(
165
+ `cd ${projectName} && npm i && npm pkg set name=${projectName} ${projectDescription ? `description="${projectDescription}"` : ""} && git init --quiet`
166
+ )
167
+ })
133
168
 
134
169
  if (tailWindSupport) {
135
- execSync(`cd ${projectName} && npm i tailwindcss@4.1.4 @tailwindcss/postcss@4.1.4`, {
136
- stdio: "inherit",
170
+ runStep("Adding Tailwind CSS", () => {
171
+ runQuiet(
172
+ `cd ${projectName} && npm i tailwindcss@4.1.4 @tailwindcss/postcss@4.1.4`
173
+ )
137
174
  })
138
175
  }
139
176
 
140
- execSync(
141
- `cd ${projectName} && git add . && git commit -m "initial commit from Create Catalyst App"`,
142
- {
143
- stdio: "inherit",
144
- }
145
- )
146
-
147
- console.log(`\n${green(bold("Success!"))} created ${projectName} at ${projectPath}`)
148
- console.log("Inside this directory, you can run the following commands.")
149
-
150
- console.log(cyan(bold(" \n npm run start")))
151
- console.log(" Starts the development server ")
152
-
153
- console.log(cyan(bold("\n npm run build")))
154
- console.log(" Bundles the app for production ")
155
-
156
- console.log(cyan(bold("\n npm run serve")))
157
- console.log(" Serves the production build ")
158
-
159
- console.log("\nWe suggest you to begin, by running")
160
- console.log(` ${cyan("cd")} ${projectName} && ${cyan("npm start")} \n\n`)
177
+ runStep("Creating initial commit", () => {
178
+ runQuiet(
179
+ `cd ${projectName} && git add . && git commit -m "initial commit from Create Catalyst App"`
180
+ )
181
+ })
161
182
 
183
+ let mcpEntry = null
162
184
  if (mcpSupport) {
163
185
  const newMcpDir = path.join(
164
186
  process.cwd(),
@@ -167,11 +189,45 @@ const program = new Commander.Command()
167
189
  "catalyst-core",
168
190
  "mcp_v2"
169
191
  )
170
- runMcpSetup(newMcpDir, path.join(process.cwd(), projectName))
192
+ // Same treatment as the install: the setup script's
193
+ // own transcript (npm warnings, audit notices, DB
194
+ // paths) only matters when it fails.
195
+ const transcript = runStep("Setting up the MCP server", () =>
196
+ runMcpSetup(newMcpDir, path.join(process.cwd(), projectName), { quiet: true })
197
+ )
198
+ for (const warning of mcpWarnings(transcript)) clack.log.warn(warning)
199
+ mcpEntry = path.join(newMcpDir, "mcp.js")
200
+ }
201
+
202
+ clack.note(
203
+ [
204
+ `${cyan("npm start".padEnd(16))}${gray("Start the development server")}`,
205
+ `${cyan("npm run build".padEnd(16))}${gray("Bundle the app for production")}`,
206
+ `${cyan("npm run serve".padEnd(16))}${gray("Serve the production build")}`,
207
+ ].join("\n"),
208
+ `Created ${bold(projectName)}`
209
+ )
210
+
211
+ if (mcpEntry) {
212
+ // The one thing from the MCP setup a person has to act
213
+ // on: the entry to paste into their MCP client.
214
+ clack.note(
215
+ [
216
+ gray("Add to your MCP client (Claude, Cursor, Windsurf):"),
217
+ `"catalyst-mcp": {`,
218
+ ` "command": "node",`,
219
+ ` "args": ["${mcpEntry}"]`,
220
+ `}`,
221
+ ].join("\n"),
222
+ "MCP server"
223
+ )
171
224
  }
225
+
226
+ clack.outro(`Next: ${cyan(`cd ${projectName}`)} && ${cyan("npm start")}`)
172
227
  } catch (error) {
228
+ // The step that failed already printed its own output.
173
229
  const catalystError = error instanceof CCAError ? error : wrapForeignError(error)
174
- printError(catalystError)
230
+ cancelWithError(catalystError, `Could not create ${projectName}: ${error.message}`)
175
231
  process.exit(1)
176
232
  } finally {
177
233
  deleteDirectory(tempDir)
@@ -238,8 +294,6 @@ const program = new Commander.Command()
238
294
  } catch (e) {
239
295
  throw createError("CCA-007", { cause: e })
240
296
  }
241
-
242
- console.log(cyan(`Run cd ${projectName} && npm start to get started.`))
243
297
  }
244
298
  } catch (error) {
245
299
  const catalystError = error instanceof CCAError ? error : wrapForeignError(error)
@@ -286,7 +340,13 @@ program
286
340
 
287
341
  program.parse(process.argv)
288
342
 
289
- function runMcpSetup(mcpDir, cwd = process.cwd()) {
343
+ /**
344
+ * @param {string} mcpDir
345
+ * @param {string} [cwd]
346
+ * @param {{quiet?: boolean}} [options] - quiet captures the output and returns
347
+ * it (printed only on failure); otherwise it streams, for `catalyst-mcp`.
348
+ */
349
+ function runMcpSetup(mcpDir, cwd = process.cwd(), { quiet = false } = {}) {
290
350
  const pkgPath = `${mcpDir}${path.sep}package.json`
291
351
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
292
352
  pkg.dependencies["better-sqlite3"] = "^12.8.0"
@@ -295,113 +355,180 @@ function runMcpSetup(mcpDir, cwd = process.cwd()) {
295
355
  const mcpNodeModules = `${mcpDir}${path.sep}node_modules`
296
356
  let needsInstall = !fs.existsSync(mcpNodeModules)
297
357
 
358
+ const say = (message) => (quiet ? null : console.log(cyan(message)))
359
+
298
360
  if (!needsInstall) {
299
361
  try {
300
362
  execSync(`node -e "require('better-sqlite3')"`, { cwd: mcpDir, stdio: "pipe" })
301
363
  } catch {
302
- console.log(cyan("Detected incompatible better-sqlite3 build. Reinstalling..."))
364
+ say("Detected incompatible better-sqlite3 build. Reinstalling...")
303
365
  fs.rmSync(mcpNodeModules, { recursive: true, force: true })
304
366
  needsInstall = true
305
367
  }
306
368
  }
307
369
 
308
- if (needsInstall) {
309
- console.log(cyan("Installing mcp_v2 dependencies..."))
310
- execSync("npm install", { cwd: mcpDir, stdio: "inherit" })
370
+ if (!quiet) {
371
+ if (needsInstall) {
372
+ say("Installing mcp_v2 dependencies...")
373
+ execSync("npm install", { cwd: mcpDir, stdio: "inherit" })
374
+ }
375
+ execFileSync(process.execPath, [`${mcpDir}${path.sep}setup.js`], { cwd, stdio: "inherit" })
376
+ return ""
311
377
  }
312
378
 
313
- execFileSync(process.execPath, [`${mcpDir}${path.sep}setup.js`], { cwd, stdio: "inherit" })
379
+ let transcript = ""
380
+ if (needsInstall) transcript += runQuiet(`npm install --no-audit --no-fund`, { cwd: mcpDir })
381
+ transcript += runQuiet(`"${process.execPath}" "${mcpDir}${path.sep}setup.js"`, { cwd })
382
+ return transcript
314
383
  }
315
384
 
316
- async function promptStateManagement() {
317
- const response = await prompts({
318
- type: "select",
319
- name: "stateManagement",
320
- message: "Choose state management:",
321
- choices: [
322
- { title: "Redux", value: "redux" },
323
- { title: "Redux Toolkit (RTK)", value: "rtk" },
324
- { title: "None", value: "none" },
325
- ],
326
- })
385
+ /**
386
+ * Warnings worth surfacing from an otherwise-hidden MCP setup transcript, e.g.
387
+ * a knowledge-base sync that could not reach GitHub. The setup marks them ⚠.
388
+ */
389
+ function mcpWarnings(transcript) {
390
+ const lines = String(transcript || "").split("\n")
391
+ const warnings = []
392
+ for (const [index, line] of lines.entries()) {
393
+ const match = /^\s*⚠\s*(.+)$/.exec(line)
394
+ if (!match) continue
395
+ const next = lines[index + 1]?.trim()
396
+ warnings.push(next && !/^[⚠✓]/.test(next) ? `${match[1]}. ${next}` : match[1])
397
+ }
398
+ return warnings
399
+ }
327
400
 
328
- return response.stateManagement
401
+ /**
402
+ * Run a command with its output captured rather than inherited.
403
+ *
404
+ * On failure the captured output is printed, so a broken install still shows
405
+ * npm's actual error instead of just a non-zero exit code.
406
+ */
407
+ function runQuiet(command, { cwd } = {}) {
408
+ try {
409
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Internal scaffolding commands built from a validated project name.
410
+ return execSync(command, { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8", cwd })
411
+ } catch (error) {
412
+ const details = [error.stdout, error.stderr].filter(Boolean).join("\n").trim()
413
+ if (details) {
414
+ process.stderr.write(`\n${details}\n`)
415
+ }
416
+ throw error
417
+ }
329
418
  }
330
- async function promptProjectName() {
331
- const res = await prompts({
332
- type: "text",
333
- name: "path",
334
- message: "What is your project named?",
335
- initial: "my-app",
336
- })
337
419
 
338
- if (typeof res.path === "string") {
339
- let projectName = res.path.trim()
340
- return projectName
420
+ /**
421
+ * Announce a step, run it, and report the outcome.
422
+ *
423
+ * Deliberately not clack's spinner: the work below is synchronous (execSync),
424
+ * so a spinner's timer never fires and the user stares at one frozen frame for
425
+ * the whole of `npm i` -- which reads as a hang. A static line that says what
426
+ * is happening is honest; a stopped animation is not.
427
+ */
428
+ function runStep(message, fn) {
429
+ // Announced before, not after: the work below is synchronous (execSync),
430
+ // so nothing else can report progress while `npm i` runs for minutes. One
431
+ // line per step -- the failure branch is what changes the outcome, and it
432
+ // says so explicitly.
433
+ clack.log.step(message)
434
+ try {
435
+ return fn()
436
+ } catch (error) {
437
+ clack.log.error(`${message} failed`)
438
+ throw error
341
439
  }
440
+ }
342
441
 
343
- if (!res.path || projectName === "") {
344
- console.log(
345
- "\nPlease specify the project directory:\n" +
346
- ` ${cyan(program.name())} ${green("<project-directory>")}\n` +
347
- "For example:\n" +
348
- ` ${cyan(program.name())} ${green("my-next-app")}\n\n` +
349
- `Run ${cyan(`${program.name()} --help`)} to see all options.`
350
- )
351
- process.exit(1)
442
+ /**
443
+ * Exit cleanly when the user hits Ctrl-C.
444
+ *
445
+ * The previous prompt library returned undefined on cancel and the scaffolder
446
+ * carried on with it, producing a half-configured project from an answer the
447
+ * user never gave.
448
+ */
449
+ function exitIfCancelled(value) {
450
+ if (clack.isCancel(value)) {
451
+ clack.cancel("Cancelled — nothing was created.")
452
+ process.exit(130)
352
453
  }
454
+ return value
353
455
  }
354
456
 
355
- async function promptDescription() {
356
- const res = await prompts({
357
- type: "text",
358
- name: "path",
359
- message: "What is your project description?",
360
- })
457
+ async function promptStateManagement() {
458
+ return exitIfCancelled(
459
+ await clack.select({
460
+ message: "Choose state management",
461
+ options: [
462
+ { value: "none", label: "None" },
463
+ { value: "redux", label: "Redux" },
464
+ { value: "rtk", label: "Redux Toolkit (RTK)" },
465
+ ],
466
+ initialValue: "none",
467
+ })
468
+ )
469
+ }
470
+
471
+ async function promptProjectName() {
472
+ return exitIfCancelled(
473
+ await clack.text({
474
+ message: "What is your project named?",
475
+ placeholder: "my-app",
476
+ defaultValue: "my-app",
477
+ validate(value) {
478
+ const name = (value || "my-app").trim()
479
+ const result = validate(name)
480
+ if (!result.validForNewPackages) {
481
+ return (result.errors || result.warnings || ["Invalid package name"])[0]
482
+ }
483
+ if (fs.existsSync(path.join(process.cwd(), name))) {
484
+ return `${name} already exists in this directory`
485
+ }
486
+ return undefined
487
+ },
488
+ })
489
+ ).trim()
490
+ }
361
491
 
362
- if (typeof res.path === "string") {
363
- return res.path.trim()
364
- } else return null
492
+ async function promptDescription() {
493
+ const value = exitIfCancelled(
494
+ await clack.text({
495
+ message: "What is your project description?",
496
+ placeholder: "optional",
497
+ defaultValue: "",
498
+ })
499
+ )
500
+ return value ? value.trim() : null
365
501
  }
366
502
 
367
503
  async function promptTypescript() {
368
- const response = await prompts({
369
- type: "select",
370
- name: "typescript",
371
- message: "Would you like to use TypeScript?",
372
- choices: [
373
- { title: "Yes", value: "ts" },
374
- { title: "No", value: "js" },
375
- ],
376
- })
377
-
378
- return response.typescript
504
+ return exitIfCancelled(
505
+ await clack.select({
506
+ message: "Would you like to use TypeScript?",
507
+ options: [
508
+ { value: "js", label: "JavaScript" },
509
+ { value: "ts", label: "TypeScript" },
510
+ ],
511
+ initialValue: "js",
512
+ })
513
+ )
379
514
  }
380
515
 
381
516
  async function promptTailwind() {
382
- const response = await prompts({
383
- type: "select",
384
- name: "tailwind",
385
- message: "Would you like to use Tailwind CSS?",
386
- choices: [
387
- { title: "Yes", value: true },
388
- { title: "No", value: false },
389
- ],
390
- })
391
- return response.tailwind
517
+ return exitIfCancelled(
518
+ await clack.confirm({
519
+ message: "Would you like to use Tailwind CSS?",
520
+ initialValue: false,
521
+ })
522
+ )
392
523
  }
393
524
 
394
525
  async function promptMcp() {
395
- const response = await prompts({
396
- type: "select",
397
- name: "mcp",
398
- message: "Would you like to setup an MCP server?",
399
- choices: [
400
- { title: "Yes", value: true },
401
- { title: "No", value: false },
402
- ],
403
- })
404
- return response.mcp
526
+ return exitIfCancelled(
527
+ await clack.confirm({
528
+ message: "Would you like to set up an MCP server?",
529
+ initialValue: true,
530
+ })
531
+ )
405
532
  }
406
533
 
407
534
  function validateOptions(cmd) {
@@ -193,7 +193,7 @@ function formatCauseChain(cause) {
193
193
  */
194
194
  function formatVerbose(err) {
195
195
  const lines = [
196
- pc.bold(`❌ ${err.message}`),
196
+ pc.bold(err.message),
197
197
  `Code: ${err.code}`,
198
198
  `Category: ${err.category || "UNKNOWN"}`,
199
199
  `Time: ${new Date().toISOString()}`,
@@ -221,7 +221,7 @@ function formatVerbose(err) {
221
221
  */
222
222
  function formatDebug(err, env) {
223
223
  const lines = [
224
- pc.bold(`❌ ${err.message}`),
224
+ pc.bold(err.message),
225
225
  `Code: ${err.code}`,
226
226
  `Category: ${err.category || "UNKNOWN"}`,
227
227
  `Time: ${new Date().toISOString()}`,
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "create-catalyst-app-starter",
3
3
  "version": "1.0.0",
4
+ "type": "module",
4
5
  "scripts": {
5
6
  "start": "catalyst start",
6
7
  "build": "catalyst build",
@@ -22,7 +23,7 @@
22
23
  "@routes": "src/js/routes/"
23
24
  },
24
25
  "dependencies": {
25
- "catalyst-core": "1.0.2-canary.1",
26
+ "catalyst-core": "1.0.2-canary.2",
26
27
  "react": "19.0.0",
27
28
  "react-dom": "19.0.0",
28
29
  "react-router": "^7.18.2"
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "create-catalyst-app-starter",
3
3
  "version": "1.0.0",
4
+ "type": "module",
4
5
  "scripts": {
5
6
  "start": "catalyst start",
6
7
  "build": "npm run typecheck && catalyst build",
@@ -23,7 +24,7 @@
23
24
  "@routes": "src/js/routes/"
24
25
  },
25
26
  "dependencies": {
26
- "catalyst-core": "1.0.2-canary.1",
27
+ "catalyst-core": "1.0.2-canary.2",
27
28
  "react": "19.0.0",
28
29
  "react-dom": "19.0.0",
29
30
  "react-router": "^7.18.2"
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "create-catalyst-app-starter",
3
3
  "version": "1.0.0",
4
+ "type": "module",
4
5
  "scripts": {
5
6
  "start": "catalyst start",
6
7
  "build": "catalyst build",
@@ -25,7 +26,7 @@
25
26
  "dependencies": {
26
27
  "@reduxjs/toolkit": "^2.9.0",
27
28
  "react-redux": "^9.2.0",
28
- "catalyst-core": "1.0.2-canary.1",
29
+ "catalyst-core": "1.0.2-canary.2",
29
30
  "react": "19.0.0",
30
31
  "react-dom": "19.0.0",
31
32
  "react-router": "^7.18.2",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "create-catalyst-app-starter",
3
3
  "version": "1.0.0",
4
+ "type": "module",
4
5
  "scripts": {
5
6
  "start": "catalyst start",
6
7
  "build": "npm run typecheck && catalyst build",
@@ -26,7 +27,7 @@
26
27
  "dependencies": {
27
28
  "@reduxjs/toolkit": "^2.9.0",
28
29
  "react-redux": "^9.2.0",
29
- "catalyst-core": "1.0.2-canary.1",
30
+ "catalyst-core": "1.0.2-canary.2",
30
31
  "react": "19.0.0",
31
32
  "react-dom": "19.0.0",
32
33
  "react-router": "^7.18.2",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "create-catalyst-app-starter",
3
3
  "version": "1.0.0",
4
+ "type": "module",
4
5
  "scripts": {
5
6
  "start": "catalyst start",
6
7
  "build": "catalyst build",
@@ -25,7 +26,7 @@
25
26
  "dependencies": {
26
27
  "@reduxjs/toolkit": "^2.9.0",
27
28
  "react-redux": "^9.2.0",
28
- "catalyst-core": "1.0.2-canary.1",
29
+ "catalyst-core": "1.0.2-canary.2",
29
30
  "react": "19.0.0",
30
31
  "react-dom": "19.0.0",
31
32
  "react-router": "^7.18.2",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "create-catalyst-app-starter",
3
3
  "version": "1.0.0",
4
+ "type": "module",
4
5
  "scripts": {
5
6
  "start": "catalyst start",
6
7
  "build": "npm run typecheck && catalyst build",
@@ -26,7 +27,7 @@
26
27
  "dependencies": {
27
28
  "@reduxjs/toolkit": "^2.9.0",
28
29
  "react-redux": "^9.2.0",
29
- "catalyst-core": "1.0.2-canary.1",
30
+ "catalyst-core": "1.0.2-canary.2",
30
31
  "react": "19.0.0",
31
32
  "react-dom": "19.0.0",
32
33
  "react-router": "^7.18.2",
@@ -0,0 +1,38 @@
1
+ // engines allows Node >=20.12, but require() of an ES module is only on by
2
+ // default from 20.19 / 22.12. On older runtimes a top-level require() of an
3
+ // ESM-only dependency (e.g. @clack/prompts) kills `npx create-catalyst-app`
4
+ // with ERR_REQUIRE_ESM before it prints anything. Simulate those runtimes by
5
+ // turning require(esm) off and check the CLI still starts.
6
+ const { spawnSync }: typeof import("child_process") = require("child_process")
7
+ const path: typeof import("path") = require("path")
8
+
9
+ const cli = path.join(__dirname, "..", "scripts", "cli.cjs")
10
+ const canDisableRequireEsm = process.allowedNodeEnvironmentFlags.has("--no-experimental-require-module")
11
+
12
+ describe.runIf(canDisableRequireEsm)("cli.cjs without require(esm)", () => {
13
+ it("prints --help", () => {
14
+ const result = spawnSync(process.execPath, ["--no-experimental-require-module", cli, "--help"], {
15
+ encoding: "utf8",
16
+ })
17
+ expect(result.stderr).not.toContain("ERR_REQUIRE_ESM")
18
+ expect(result.status).toBe(0)
19
+ expect(result.stdout).toContain("Usage:")
20
+ })
21
+
22
+ it("loads the prompt library when scaffolding", () => {
23
+ // An invalid name fails validation right after the prompt library is
24
+ // imported, so nothing is created.
25
+ const result = spawnSync(
26
+ process.execPath,
27
+ ["--no-experimental-require-module", cli, "Bad Name", "-y"],
28
+ {
29
+ encoding: "utf8",
30
+ env: { ...process.env, NO_COLOR: "1" },
31
+ }
32
+ )
33
+ const output = result.stdout + result.stderr
34
+ expect(output).not.toContain("ERR_REQUIRE_ESM")
35
+ expect(output).toContain("CCA-001")
36
+ expect(result.status).toBe(1)
37
+ })
38
+ })