create-catalyst-app 1.0.2-canary.1 → 1.0.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",
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,9 +18,10 @@
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
- "jscodeshift": "^0.16.0",
24
+ "jscodeshift": "^17.4.0",
21
25
  "prompts": "^2.4.0",
22
26
  "tar": "^7.5.22",
23
27
  "validate-npm-package-name": "^5.0.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,37 @@ 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
  }
112
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - projectName is checked by validate-npm-package-name (no separators or ..) before use.
85
113
  let projectPath = path.join(process.cwd(), projectName)
86
114
  if (fs.existsSync(projectPath)) {
87
- printError(
115
+ cancelWithError(
88
116
  createError("CCA-002", {
89
- message: `${projectName} already exists, try again.`,
117
+ message: `${projectName} already exists — choose another name or remove it.`,
90
118
  })
91
119
  )
92
120
  process.exit(1)
93
121
  }
94
122
  const projectDescription = config.description || (await promptDescription())
95
123
  const language = config.language || (await promptTypescript())
96
- const tailWindSupport = config.tailWindSupport !== null || (await promptTailwind())
124
+ // These are booleans, so `!== null` collapsed a configured `false`
125
+ // into `true` -- `--yes` installed Tailwind against its own default.
126
+ // Only fall through to the prompt when nothing was configured.
127
+ const tailWindSupport =
128
+ config.tailWindSupport != null ? config.tailWindSupport : await promptTailwind()
97
129
  const stateManagement = config.stateManagement || (await promptStateManagement())
98
- const mcpSupport = config.mcpSupport !== null || (await promptMcp())
130
+ const mcpSupport = config.mcpSupport != null ? config.mcpSupport : await promptMcp()
99
131
 
100
132
  // Define mapping of options to repository suffixes
101
133
  const repositorySuffixes = {
@@ -126,52 +158,80 @@ const program = new Commander.Command()
126
158
  extractSubdirectory(packageFilePath)
127
159
  createGitignore(projectName)
128
160
 
129
- execSync(
130
- `cd ${projectName} && npm i && npm pkg set name=${projectName} ${projectDescription ? `description="${projectDescription}"` : ""} && git init --quiet`,
131
- { stdio: "inherit" }
132
- )
161
+ // npm's install output is hundreds of lines nobody reads
162
+ // when it works. Fold it behind a spinner, and print it in
163
+ // full only if the step actually fails.
164
+ runStep(`Installing dependencies`, () => {
165
+ runQuiet(
166
+ `cd ${projectName} && npm i && npm pkg set name=${projectName} ${projectDescription ? `description="${projectDescription}"` : ""} && git init --quiet`
167
+ )
168
+ })
133
169
 
134
170
  if (tailWindSupport) {
135
- execSync(`cd ${projectName} && npm i tailwindcss@4.1.4 @tailwindcss/postcss@4.1.4`, {
136
- stdio: "inherit",
171
+ runStep("Adding Tailwind CSS", () => {
172
+ runQuiet(
173
+ `cd ${projectName} && npm i tailwindcss@4.1.4 @tailwindcss/postcss@4.1.4`
174
+ )
137
175
  })
138
176
  }
139
177
 
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`)
178
+ runStep("Creating initial commit", () => {
179
+ runQuiet(
180
+ `cd ${projectName} && git add . && git commit -m "initial commit from Create Catalyst App"`
181
+ )
182
+ })
161
183
 
184
+ let mcpEntry = null
162
185
  if (mcpSupport) {
163
186
  const newMcpDir = path.join(
164
187
  process.cwd(),
165
- projectName,
188
+ projectName, // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - projectName is checked by validate-npm-package-name (no separators or ..) before use.
166
189
  "node_modules",
167
190
  "catalyst-core",
168
191
  "mcp_v2"
169
192
  )
170
- runMcpSetup(newMcpDir, path.join(process.cwd(), projectName))
193
+ // Same treatment as the install: the setup script's
194
+ // own transcript (npm warnings, audit notices, DB
195
+ // paths) only matters when it fails.
196
+ const transcript = runStep(
197
+ "Setting up the MCP server",
198
+ () =>
199
+ runMcpSetup(newMcpDir, path.join(process.cwd(), projectName), { quiet: true }) // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - projectName is checked by validate-npm-package-name (no separators or ..) before use.
200
+ )
201
+ for (const warning of mcpWarnings(transcript)) clack.log.warn(warning)
202
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - newMcpDir is built from the validated projectName.
203
+ mcpEntry = path.join(newMcpDir, "mcp.js")
204
+ }
205
+
206
+ clack.note(
207
+ [
208
+ `${cyan("npm start".padEnd(16))}${gray("Start the development server")}`,
209
+ `${cyan("npm run build".padEnd(16))}${gray("Bundle the app for production")}`,
210
+ `${cyan("npm run serve".padEnd(16))}${gray("Serve the production build")}`,
211
+ ].join("\n"),
212
+ `Created ${bold(projectName)}`
213
+ )
214
+
215
+ if (mcpEntry) {
216
+ // The one thing from the MCP setup a person has to act
217
+ // on: the entry to paste into their MCP client.
218
+ clack.note(
219
+ [
220
+ gray("Add to your MCP client (Claude, Cursor, Windsurf):"),
221
+ `"catalyst-mcp": {`,
222
+ ` "command": "node",`,
223
+ ` "args": ["${mcpEntry}"]`,
224
+ `}`,
225
+ ].join("\n"),
226
+ "MCP server"
227
+ )
171
228
  }
229
+
230
+ clack.outro(`Next: ${cyan(`cd ${projectName}`)} && ${cyan("npm start")}`)
172
231
  } catch (error) {
232
+ // The step that failed already printed its own output.
173
233
  const catalystError = error instanceof CCAError ? error : wrapForeignError(error)
174
- printError(catalystError)
234
+ cancelWithError(catalystError, `Could not create ${projectName}: ${error.message}`)
175
235
  process.exit(1)
176
236
  } finally {
177
237
  deleteDirectory(tempDir)
@@ -238,8 +298,6 @@ const program = new Commander.Command()
238
298
  } catch (e) {
239
299
  throw createError("CCA-007", { cause: e })
240
300
  }
241
-
242
- console.log(cyan(`Run cd ${projectName} && npm start to get started.`))
243
301
  }
244
302
  } catch (error) {
245
303
  const catalystError = error instanceof CCAError ? error : wrapForeignError(error)
@@ -286,7 +344,13 @@ program
286
344
 
287
345
  program.parse(process.argv)
288
346
 
289
- function runMcpSetup(mcpDir, cwd = process.cwd()) {
347
+ /**
348
+ * @param {string} mcpDir
349
+ * @param {string} [cwd]
350
+ * @param {{quiet?: boolean}} [options] - quiet captures the output and returns
351
+ * it (printed only on failure); otherwise it streams, for `catalyst-mcp`.
352
+ */
353
+ function runMcpSetup(mcpDir, cwd = process.cwd(), { quiet = false } = {}) {
290
354
  const pkgPath = `${mcpDir}${path.sep}package.json`
291
355
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
292
356
  pkg.dependencies["better-sqlite3"] = "^12.8.0"
@@ -295,113 +359,181 @@ function runMcpSetup(mcpDir, cwd = process.cwd()) {
295
359
  const mcpNodeModules = `${mcpDir}${path.sep}node_modules`
296
360
  let needsInstall = !fs.existsSync(mcpNodeModules)
297
361
 
362
+ const say = (message) => (quiet ? null : console.log(cyan(message)))
363
+
298
364
  if (!needsInstall) {
299
365
  try {
300
366
  execSync(`node -e "require('better-sqlite3')"`, { cwd: mcpDir, stdio: "pipe" })
301
367
  } catch {
302
- console.log(cyan("Detected incompatible better-sqlite3 build. Reinstalling..."))
368
+ say("Detected incompatible better-sqlite3 build. Reinstalling...")
303
369
  fs.rmSync(mcpNodeModules, { recursive: true, force: true })
304
370
  needsInstall = true
305
371
  }
306
372
  }
307
373
 
308
- if (needsInstall) {
309
- console.log(cyan("Installing mcp_v2 dependencies..."))
310
- execSync("npm install", { cwd: mcpDir, stdio: "inherit" })
374
+ if (!quiet) {
375
+ if (needsInstall) {
376
+ say("Installing mcp_v2 dependencies...")
377
+ execSync("npm install", { cwd: mcpDir, stdio: "inherit" })
378
+ }
379
+ execFileSync(process.execPath, [`${mcpDir}${path.sep}setup.js`], { cwd, stdio: "inherit" })
380
+ return ""
311
381
  }
312
382
 
313
- execFileSync(process.execPath, [`${mcpDir}${path.sep}setup.js`], { cwd, stdio: "inherit" })
383
+ let transcript = ""
384
+ if (needsInstall) transcript += runQuiet(`npm install --no-audit --no-fund`, { cwd: mcpDir })
385
+ transcript += runQuiet(`"${process.execPath}" "${mcpDir}${path.sep}setup.js"`, { cwd })
386
+ return transcript
314
387
  }
315
388
 
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
- })
389
+ /**
390
+ * Warnings worth surfacing from an otherwise-hidden MCP setup transcript, e.g.
391
+ * a knowledge-base sync that could not reach GitHub. The setup marks them ⚠.
392
+ */
393
+ function mcpWarnings(transcript) {
394
+ const lines = String(transcript || "").split("\n")
395
+ const warnings = []
396
+ for (const [index, line] of lines.entries()) {
397
+ const match = /^\s*⚠\s*(.+)$/.exec(line)
398
+ if (!match) continue
399
+ const next = lines[index + 1]?.trim()
400
+ warnings.push(next && !/^[⚠✓]/.test(next) ? `${match[1]}. ${next}` : match[1])
401
+ }
402
+ return warnings
403
+ }
327
404
 
328
- return response.stateManagement
405
+ /**
406
+ * Run a command with its output captured rather than inherited.
407
+ *
408
+ * On failure the captured output is printed, so a broken install still shows
409
+ * npm's actual error instead of just a non-zero exit code.
410
+ */
411
+ function runQuiet(command, { cwd } = {}) {
412
+ try {
413
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Internal scaffolding commands built from a validated project name.
414
+ return execSync(command, { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8", cwd })
415
+ } catch (error) {
416
+ const details = [error.stdout, error.stderr].filter(Boolean).join("\n").trim()
417
+ if (details) {
418
+ process.stderr.write(`\n${details}\n`)
419
+ }
420
+ throw error
421
+ }
329
422
  }
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
423
 
338
- if (typeof res.path === "string") {
339
- let projectName = res.path.trim()
340
- return projectName
424
+ /**
425
+ * Announce a step, run it, and report the outcome.
426
+ *
427
+ * Deliberately not clack's spinner: the work below is synchronous (execSync),
428
+ * so a spinner's timer never fires and the user stares at one frozen frame for
429
+ * the whole of `npm i` -- which reads as a hang. A static line that says what
430
+ * is happening is honest; a stopped animation is not.
431
+ */
432
+ function runStep(message, fn) {
433
+ // Announced before, not after: the work below is synchronous (execSync),
434
+ // so nothing else can report progress while `npm i` runs for minutes. One
435
+ // line per step -- the failure branch is what changes the outcome, and it
436
+ // says so explicitly.
437
+ clack.log.step(message)
438
+ try {
439
+ return fn()
440
+ } catch (error) {
441
+ clack.log.error(`${message} failed`)
442
+ throw error
341
443
  }
444
+ }
342
445
 
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)
446
+ /**
447
+ * Exit cleanly when the user hits Ctrl-C.
448
+ *
449
+ * The previous prompt library returned undefined on cancel and the scaffolder
450
+ * carried on with it, producing a half-configured project from an answer the
451
+ * user never gave.
452
+ */
453
+ function exitIfCancelled(value) {
454
+ if (clack.isCancel(value)) {
455
+ clack.cancel("Cancelled — nothing was created.")
456
+ process.exit(130)
352
457
  }
458
+ return value
353
459
  }
354
460
 
355
- async function promptDescription() {
356
- const res = await prompts({
357
- type: "text",
358
- name: "path",
359
- message: "What is your project description?",
360
- })
461
+ async function promptStateManagement() {
462
+ return exitIfCancelled(
463
+ await clack.select({
464
+ message: "Choose state management",
465
+ options: [
466
+ { value: "none", label: "None" },
467
+ { value: "redux", label: "Redux" },
468
+ { value: "rtk", label: "Redux Toolkit (RTK)" },
469
+ ],
470
+ initialValue: "none",
471
+ })
472
+ )
473
+ }
474
+
475
+ async function promptProjectName() {
476
+ return exitIfCancelled(
477
+ await clack.text({
478
+ message: "What is your project named?",
479
+ placeholder: "my-app",
480
+ defaultValue: "my-app",
481
+ validate(value) {
482
+ const name = (value || "my-app").trim()
483
+ const result = validate(name)
484
+ if (!result.validForNewPackages) {
485
+ return (result.errors || result.warnings || ["Invalid package name"])[0]
486
+ }
487
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - projectName is checked by validate-npm-package-name (no separators or ..) before use.
488
+ if (fs.existsSync(path.join(process.cwd(), name))) {
489
+ return `${name} already exists in this directory`
490
+ }
491
+ return undefined
492
+ },
493
+ })
494
+ ).trim()
495
+ }
361
496
 
362
- if (typeof res.path === "string") {
363
- return res.path.trim()
364
- } else return null
497
+ async function promptDescription() {
498
+ const value = exitIfCancelled(
499
+ await clack.text({
500
+ message: "What is your project description?",
501
+ placeholder: "optional",
502
+ defaultValue: "",
503
+ })
504
+ )
505
+ return value ? value.trim() : null
365
506
  }
366
507
 
367
508
  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
509
+ return exitIfCancelled(
510
+ await clack.select({
511
+ message: "Would you like to use TypeScript?",
512
+ options: [
513
+ { value: "js", label: "JavaScript" },
514
+ { value: "ts", label: "TypeScript" },
515
+ ],
516
+ initialValue: "js",
517
+ })
518
+ )
379
519
  }
380
520
 
381
521
  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
522
+ return exitIfCancelled(
523
+ await clack.confirm({
524
+ message: "Would you like to use Tailwind CSS?",
525
+ initialValue: false,
526
+ })
527
+ )
392
528
  }
393
529
 
394
530
  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
531
+ return exitIfCancelled(
532
+ await clack.confirm({
533
+ message: "Would you like to set up an MCP server?",
534
+ initialValue: true,
535
+ })
536
+ )
405
537
  }
406
538
 
407
539
  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",
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",
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",
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",
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",
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",
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
+ })