create-cascade 0.1.4 → 0.1.6

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 +8 -7
  2. package/index.js +203 -59
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -13,7 +13,7 @@ bun create cascade
13
13
  - Create in a new folder or directly in the current folder
14
14
  - Interactive framework selection (`core`, `react`, `solid`)
15
15
  - Multiple built-in starter code presets per framework
16
- - Optional direct install and immediate app start
16
+ - Scaffold only by default (no install, no auto-run)
17
17
 
18
18
  ## Frameworks
19
19
 
@@ -48,8 +48,8 @@ Options:
48
48
  -f, --framework <name> Framework: core, react, solid
49
49
  -s, --starter <name> Starter preset for selected framework
50
50
  --here Use current directory
51
- --no-install Skip bun install
52
- --no-start Skip bun run dev after install
51
+ --install Run bun install after scaffolding
52
+ --start Run bun install, then bun run dev
53
53
  -h, --help Show help
54
54
  ```
55
55
 
@@ -65,14 +65,15 @@ bun create cascade --here
65
65
  # Create React app with counter starter
66
66
  bun create cascade my-app -f react -s counter
67
67
 
68
- # Scaffold only, no install
69
- bun create cascade my-app --no-install
68
+ # Scaffold then install
69
+ bun create cascade my-app --install
70
70
  ```
71
71
 
72
72
  ## Behavior
73
73
 
74
- - If installation is enabled, the CLI runs `bun install` in the target directory.
75
- - If installation is enabled and `--no-start` is not set, the CLI starts the app with `bun run dev`.
74
+ - By default, the CLI only scaffolds files.
75
+ - With `--install`, it runs `bun install` in the target directory.
76
+ - With `--start`, it runs `bun install` then `bun run dev`.
76
77
 
77
78
  ## Package
78
79
 
package/index.js CHANGED
@@ -5,6 +5,12 @@ import { basename, resolve } from "node:path"
5
5
  import process from "node:process"
6
6
  import { spawnSync } from "node:child_process"
7
7
  import { createInterface } from "node:readline/promises"
8
+ import { emitKeypressEvents } from "node:readline"
9
+
10
+ const ANSI_RESET = "\x1b[0m"
11
+ const ANSI_BOLD = "\x1b[1m"
12
+ const ANSI_DIM = "\x1b[2m"
13
+ const ANSI_CYAN = "\x1b[36m"
8
14
 
9
15
  const FRAMEWORKS = [
10
16
  { id: "core", label: "Core", description: "Vanilla Cascade API with renderables" },
@@ -14,17 +20,17 @@ const FRAMEWORKS = [
14
20
 
15
21
  const STARTERS = {
16
22
  core: [
17
- { id: "minimal", label: "Minimal", description: "Single welcome message" },
23
+ { id: "minimal", label: "Minimal", description: "Welcome panel with quick next steps" },
18
24
  { id: "counter", label: "Counter", description: "Live counter updated every second" },
19
25
  { id: "layout", label: "Layout", description: "Simple boxed layout starter" },
20
26
  ],
21
27
  react: [
22
- { id: "minimal", label: "Minimal", description: "Render a single text node" },
28
+ { id: "minimal", label: "Minimal", description: "Welcome panel with quick next steps" },
23
29
  { id: "counter", label: "Counter", description: "React state with interval updates" },
24
30
  { id: "login", label: "Login", description: "Small interactive login form" },
25
31
  ],
26
32
  solid: [
27
- { id: "minimal", label: "Minimal", description: "Render a single text node" },
33
+ { id: "minimal", label: "Minimal", description: "Welcome panel with quick next steps" },
28
34
  { id: "counter", label: "Counter", description: "Solid signal with interval updates" },
29
35
  { id: "input", label: "Input", description: "Basic input and submit interaction" },
30
36
  ],
@@ -33,8 +39,8 @@ const STARTERS = {
33
39
  function parseArgs(argv) {
34
40
  const args = argv.slice(2)
35
41
  const options = {
36
- noInstall: false,
37
- noStart: false,
42
+ install: false,
43
+ start: false,
38
44
  here: false,
39
45
  framework: undefined,
40
46
  starter: undefined,
@@ -45,12 +51,21 @@ function parseArgs(argv) {
45
51
  for (let i = 0; i < args.length; i += 1) {
46
52
  const arg = args[i]
47
53
 
54
+ if (arg === "--install") {
55
+ options.install = true
56
+ continue
57
+ }
58
+ if (arg === "--start") {
59
+ options.start = true
60
+ options.install = true
61
+ continue
62
+ }
48
63
  if (arg === "--no-install") {
49
- options.noInstall = true
64
+ options.install = false
50
65
  continue
51
66
  }
52
67
  if (arg === "--no-start") {
53
- options.noStart = true
68
+ options.start = false
54
69
  continue
55
70
  }
56
71
  if (arg === "--here") {
@@ -84,8 +99,8 @@ function printHelp() {
84
99
  console.log(" -f, --framework <name> Framework: core, react, solid")
85
100
  console.log(" -s, --starter <name> Starter preset for selected framework")
86
101
  console.log(" --here Use current directory")
87
- console.log(" --no-install Skip bun install")
88
- console.log(" --no-start Skip bun run dev after install")
102
+ console.log(" --install Run bun install after scaffolding")
103
+ console.log(" --start Run bun install, then bun run dev")
89
104
  console.log(" -h, --help Show help")
90
105
  console.log("")
91
106
  console.log("Examples:")
@@ -118,47 +133,83 @@ function promptLine(rl, label) {
118
133
  }
119
134
 
120
135
  async function selectOption(rl, label, options) {
121
- console.log("")
122
- console.log(label)
123
- for (let i = 0; i < options.length; i += 1) {
124
- const option = options[i]
125
- console.log(` ${i + 1}. ${option.label} (${option.id}) - ${option.description}`)
136
+ if (!process.stdin.isTTY) {
137
+ return options[0].id
126
138
  }
127
139
 
128
- while (true) {
129
- const raw = (await promptLine(rl, "Select a number: ")).trim()
130
- const index = Number(raw)
131
- if (Number.isInteger(index) && index >= 1 && index <= options.length) {
132
- return options[index - 1].id
140
+ const stdin = process.stdin
141
+ const stdout = process.stdout
142
+ let selectedIndex = 0
143
+ const totalLines = options.length + 2
144
+ let renderedOnce = false
145
+
146
+ const render = () => {
147
+ if (renderedOnce) {
148
+ stdout.write(`\x1b[${totalLines}F`)
149
+ } else {
150
+ stdout.write("\n")
133
151
  }
134
- console.log("Invalid choice. Try again.")
135
- }
136
- }
137
152
 
138
- async function resolveProjectName(rl, options, positionals) {
139
- if (options.here) {
140
- return "."
141
- }
153
+ stdout.write(`${ANSI_BOLD}${label}${ANSI_RESET}\n`)
142
154
 
143
- if (positionals[0]) {
144
- return positionals[0]
145
- }
155
+ for (let i = 0; i < options.length; i += 1) {
156
+ const option = options[i]
157
+ const isSelected = i === selectedIndex
158
+ const prefix = isSelected ? `${ANSI_BOLD}${ANSI_CYAN}>${ANSI_RESET}` : " "
159
+ const styleStart = isSelected ? `${ANSI_BOLD}${ANSI_CYAN}` : ""
160
+ const styleEnd = isSelected ? ANSI_RESET : ""
161
+ stdout.write(`${prefix} ${styleStart}${option.label}${styleEnd} ${ANSI_DIM}(${option.id}) - ${option.description}${ANSI_RESET}\n`)
162
+ }
146
163
 
147
- if (!process.stdin.isTTY) {
148
- return "cascade-app"
164
+ stdout.write(`${ANSI_DIM}Use Up/Down arrows and Enter${ANSI_RESET}\n`)
165
+ renderedOnce = true
149
166
  }
150
167
 
151
- const locationChoice = await selectOption(rl, "Where should the project be created?", [
152
- { id: "new", label: "New folder", description: "Create and use a new project directory" },
153
- { id: "here", label: "Current folder", description: "Use the current directory directly" },
154
- ])
168
+ return new Promise((resolve, reject) => {
169
+ const cleanup = () => {
170
+ stdin.off("keypress", onKeyPress)
171
+ if (stdin.isTTY) {
172
+ stdin.setRawMode(false)
173
+ }
174
+ stdout.write("\n")
175
+ }
155
176
 
156
- if (locationChoice === "here") {
157
- return "."
158
- }
177
+ const onKeyPress = (_, key) => {
178
+ if (!key) {
179
+ return
180
+ }
181
+
182
+ if (key.ctrl && key.name === "c") {
183
+ cleanup()
184
+ reject(new Error("Operation cancelled"))
185
+ return
186
+ }
187
+
188
+ if (key.name === "up") {
189
+ selectedIndex = selectedIndex === 0 ? options.length - 1 : selectedIndex - 1
190
+ render()
191
+ return
192
+ }
193
+
194
+ if (key.name === "down") {
195
+ selectedIndex = selectedIndex === options.length - 1 ? 0 : selectedIndex + 1
196
+ render()
197
+ return
198
+ }
199
+
200
+ if (key.name === "return") {
201
+ const selected = options[selectedIndex]
202
+ cleanup()
203
+ resolve(selected.id)
204
+ }
205
+ }
159
206
 
160
- const input = (await promptLine(rl, "Project name (default: cascade-app): ")).trim()
161
- return input || "cascade-app"
207
+ emitKeypressEvents(stdin)
208
+ stdin.setRawMode(true)
209
+ stdin.resume()
210
+ stdin.on("keypress", onKeyPress)
211
+ render()
212
+ })
162
213
  }
163
214
 
164
215
  async function resolveFramework(rl, frameworkArg) {
@@ -196,6 +247,46 @@ async function resolveStarter(rl, framework, starterArg) {
196
247
  return selectOption(rl, `Choose a starter for ${framework}:`, choices)
197
248
  }
198
249
 
250
+ async function resolveLocation(rl, options, positionals) {
251
+ if (options.here) {
252
+ return "here"
253
+ }
254
+
255
+ if (positionals[0]) {
256
+ return "new"
257
+ }
258
+
259
+ if (!process.stdin.isTTY) {
260
+ return "new"
261
+ }
262
+
263
+ return selectOption(rl, "Where should the project be created?", [
264
+ { id: "new", label: "New folder", description: "Create and use a new project directory" },
265
+ { id: "here", label: "Current folder", description: "Use the current directory directly" },
266
+ ])
267
+ }
268
+
269
+ async function resolveProjectName(rl, positionals, defaultName = "cascade-app") {
270
+ if (positionals[0]) {
271
+ return positionals[0]
272
+ }
273
+
274
+ if (!process.stdin.isTTY) {
275
+ return defaultName
276
+ }
277
+
278
+ const input = (await promptLine(rl, `Project name (default: ${defaultName}): `)).trim()
279
+ return input || defaultName
280
+ }
281
+
282
+ function shouldRunInteractiveWizard(options, positionals) {
283
+ if (!process.stdin.isTTY) {
284
+ return false
285
+ }
286
+
287
+ return !positionals[0] && !options.here && !options.framework && !options.starter
288
+ }
289
+
199
290
  function getPackageJson(projectName, framework) {
200
291
  const dependencies = {
201
292
  "@cascadetui/core": "latest",
@@ -221,6 +312,8 @@ function getPackageJson(projectName, framework) {
221
312
  type: "module",
222
313
  scripts: {
223
314
  dev: `bun run ${entry}`,
315
+ start: `bun run ${entry}`,
316
+ typecheck: "bunx tsc --noEmit",
224
317
  },
225
318
  dependencies,
226
319
  }
@@ -232,7 +325,10 @@ function getTsConfig(framework) {
232
325
  module: "ESNext",
233
326
  moduleResolution: "Bundler",
234
327
  strict: true,
328
+ noEmit: true,
329
+ verbatimModuleSyntax: true,
235
330
  skipLibCheck: true,
331
+ types: ["bun-types"],
236
332
  }
237
333
 
238
334
  if (framework === "react") {
@@ -267,17 +363,41 @@ function getTsConfig(framework) {
267
363
  function getSource(framework, starter) {
268
364
  const sources = {
269
365
  core: {
270
- minimal: `import { TextRenderable, createCliRenderer } from "@cascadetui/core"
366
+ minimal: `import { BoxRenderable, TextRenderable, createCliRenderer } from "@cascadetui/core"
271
367
 
272
368
  const renderer = await createCliRenderer({ exitOnCtrlC: true })
273
369
 
274
- const text = new TextRenderable(renderer, {
275
- content: "Hello from Cascade",
276
- margin: 2,
370
+ const panel = new BoxRenderable(renderer, {
371
+ border: true,
372
+ borderStyle: "single",
373
+ padding: 1,
374
+ margin: 1,
375
+ width: 56,
376
+ height: 8,
377
+ flexDirection: "column",
378
+ })
379
+
380
+ const title = new TextRenderable(renderer, {
381
+ content: "Hello from Cascade Core",
277
382
  fg: "#00ff99",
278
383
  })
279
384
 
280
- renderer.root.add(text)
385
+ const subtitle = new TextRenderable(renderer, {
386
+ content: "Run bun run dev after edits to refresh your app",
387
+ marginTop: 1,
388
+ fg: "#cccccc",
389
+ })
390
+
391
+ const hint = new TextRenderable(renderer, {
392
+ content: "Press Ctrl+C to exit",
393
+ marginTop: 1,
394
+ fg: "#999999",
395
+ })
396
+
397
+ panel.add(title)
398
+ panel.add(subtitle)
399
+ panel.add(hint)
400
+ renderer.root.add(panel)
281
401
  `,
282
402
  counter: `import { TextRenderable, createCliRenderer } from "@cascadetui/core"
283
403
 
@@ -332,7 +452,13 @@ renderer.root.add(container)
332
452
  import { createRoot } from "@cascadetui/react"
333
453
 
334
454
  function App() {
335
- return <text content="Hello from Cascade + React" fg="#00ff99" />
455
+ return (
456
+ <box style={{ border: true, borderStyle: "single", padding: 1, margin: 1, width: 56, height: 8, flexDirection: "column" }}>
457
+ <text content="Hello from Cascade React" fg="#00ff99" />
458
+ <text content="Edit src/index.tsx and run bun run dev" marginTop={1} fg="#cccccc" />
459
+ <text content="Press Ctrl+C to exit" marginTop={1} fg="#999999" />
460
+ </box>
461
+ )
336
462
  }
337
463
 
338
464
  const renderer = await createCliRenderer({ exitOnCtrlC: true })
@@ -405,7 +531,13 @@ createRoot(renderer).render(<App />)
405
531
  solid: {
406
532
  minimal: `import { render } from "@cascadetui/solid"
407
533
 
408
- const App = () => <text content="Hello from Cascade + Solid" fg="#00ff99" />
534
+ const App = () => (
535
+ <box style={{ border: true, borderStyle: "single", padding: 1, margin: 1, width: 56, height: 8, flexDirection: "column" }}>
536
+ <text content="Hello from Cascade Solid" fg="#00ff99" />
537
+ <text content="Edit src/index.tsx and run bun run dev" marginTop={1} fg="#cccccc" />
538
+ <text content="Press Ctrl+C to exit" marginTop={1} fg="#999999" />
539
+ </box>
540
+ )
409
541
 
410
542
  render(App, { exitOnCtrlC: true })
411
543
  `,
@@ -502,13 +634,27 @@ async function main() {
502
634
  })
503
635
 
504
636
  try {
505
- const rawProjectName = await resolveProjectName(rl, options, positionals)
506
- const framework = await resolveFramework(rl, options.framework)
507
- const starter = await resolveStarter(rl, framework, options.starter)
637
+ let framework
638
+ let projectName
639
+ let location
640
+ let starter
641
+
642
+ if (shouldRunInteractiveWizard(options, positionals)) {
643
+ framework = await resolveFramework(rl, undefined)
644
+ projectName = await resolveProjectName(rl, [], "cascade-app")
645
+ location = await resolveLocation(rl, { ...options, here: false }, [])
646
+ starter = await resolveStarter(rl, framework, undefined)
647
+ } else {
648
+ framework = await resolveFramework(rl, options.framework)
649
+ location = await resolveLocation(rl, options, positionals)
650
+ const defaultName = location === "here" ? basename(process.cwd()) : "cascade-app"
651
+ projectName = await resolveProjectName(rl, positionals, defaultName)
652
+ starter = await resolveStarter(rl, framework, options.starter)
653
+ }
508
654
 
509
- const usingCurrentDirectory = rawProjectName === "."
510
- const targetDir = usingCurrentDirectory ? process.cwd() : resolve(process.cwd(), rawProjectName)
511
- const packageNameSeed = usingCurrentDirectory ? basename(targetDir) : rawProjectName
655
+ const usingCurrentDirectory = location === "here"
656
+ const targetDir = usingCurrentDirectory ? process.cwd() : resolve(process.cwd(), projectName)
657
+ const packageNameSeed = projectName
512
658
 
513
659
  writeProject(targetDir, packageNameSeed, framework, starter)
514
660
 
@@ -517,13 +663,13 @@ async function main() {
517
663
  console.log(`Framework: ${framework}`)
518
664
  console.log(`Starter: ${starter}`)
519
665
 
520
- if (!options.noInstall) {
666
+ if (options.install) {
521
667
  console.log("")
522
668
  console.log("Installing dependencies with bun...")
523
669
  runCommand("bun", ["install"], targetDir)
524
670
  }
525
671
 
526
- if (!options.noInstall && !options.noStart) {
672
+ if (options.start) {
527
673
  console.log("")
528
674
  console.log("Starting the project...")
529
675
  runCommand("bun", ["run", "dev"], targetDir)
@@ -533,11 +679,9 @@ async function main() {
533
679
  console.log("")
534
680
  console.log("Next steps:")
535
681
  if (!usingCurrentDirectory) {
536
- console.log(` cd ${rawProjectName}`)
537
- }
538
- if (options.noInstall) {
539
- console.log(" bun install")
682
+ console.log(` cd ${projectName}`)
540
683
  }
684
+ console.log(" bun install")
541
685
  console.log(" bun run dev")
542
686
  } finally {
543
687
  rl.close()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-cascade",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Create a new Cascade TUI project",
5
5
  "type": "module",
6
6
  "license": "MIT",