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 +6 -2
- package/scripts/cli.cjs +257 -125
- package/scripts/errors.cjs +2 -2
- package/templates/none-js/package.json +2 -1
- package/templates/none-ts/package.json +2 -1
- package/templates/redux-js/package.json +2 -1
- package/templates/redux-ts/package.json +2 -1
- package/templates/rtk-js/package.json +2 -1
- package/templates/rtk-ts/package.json +2 -1
- package/test/cli-require-esm.test.ts +38 -0
- /package/templates/tailwind/{postcss.config.js → postcss.config.cjs} +0 -0
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
|
|
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": "^
|
|
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
|
-
|
|
6
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
clack.log.info("Using default configuration")
|
|
64
88
|
config = {
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
createError("CCA-001", {
|
|
80
|
-
|
|
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
|
-
|
|
115
|
+
cancelWithError(
|
|
88
116
|
createError("CCA-002", {
|
|
89
|
-
message: `${projectName} already exists
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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) {
|
package/scripts/errors.cjs
CHANGED
|
@@ -193,7 +193,7 @@ function formatCauseChain(cause) {
|
|
|
193
193
|
*/
|
|
194
194
|
function formatVerbose(err) {
|
|
195
195
|
const lines = [
|
|
196
|
-
pc.bold(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
})
|
|
File without changes
|