create-cascade 0.1.3 → 0.1.5
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/README.md +80 -0
- package/index.js +593 -45
- package/package.json +5 -5
- package/template/package.json +0 -12
- package/template/src/index.ts +0 -12
- package/template/tsconfig.json +0 -10
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Create Cascade App
|
|
2
|
+
|
|
3
|
+
A CLI tool to create Cascade projects with interactive framework and starter selection.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun create cascade
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Create in a new folder or directly in the current folder
|
|
14
|
+
- Interactive framework selection (`core`, `react`, `solid`)
|
|
15
|
+
- Multiple built-in starter code presets per framework
|
|
16
|
+
- Scaffold only by default (no install, no auto-run)
|
|
17
|
+
|
|
18
|
+
## Frameworks
|
|
19
|
+
|
|
20
|
+
- `core`: Vanilla Cascade API
|
|
21
|
+
- `react`: Cascade + React renderer
|
|
22
|
+
- `solid`: Cascade + Solid renderer
|
|
23
|
+
|
|
24
|
+
## Starter Presets
|
|
25
|
+
|
|
26
|
+
### Core
|
|
27
|
+
|
|
28
|
+
- `minimal`
|
|
29
|
+
- `counter`
|
|
30
|
+
- `layout`
|
|
31
|
+
|
|
32
|
+
### React
|
|
33
|
+
|
|
34
|
+
- `minimal`
|
|
35
|
+
- `counter`
|
|
36
|
+
- `login`
|
|
37
|
+
|
|
38
|
+
### Solid
|
|
39
|
+
|
|
40
|
+
- `minimal`
|
|
41
|
+
- `counter`
|
|
42
|
+
- `input`
|
|
43
|
+
|
|
44
|
+
## CLI Options
|
|
45
|
+
|
|
46
|
+
```txt
|
|
47
|
+
Options:
|
|
48
|
+
-f, --framework <name> Framework: core, react, solid
|
|
49
|
+
-s, --starter <name> Starter preset for selected framework
|
|
50
|
+
--here Use current directory
|
|
51
|
+
--install Run bun install after scaffolding
|
|
52
|
+
--start Run bun install, then bun run dev
|
|
53
|
+
-h, --help Show help
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Examples
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Interactive mode
|
|
60
|
+
bun create cascade
|
|
61
|
+
|
|
62
|
+
# Create in current folder
|
|
63
|
+
bun create cascade --here
|
|
64
|
+
|
|
65
|
+
# Create React app with counter starter
|
|
66
|
+
bun create cascade my-app -f react -s counter
|
|
67
|
+
|
|
68
|
+
# Scaffold then install
|
|
69
|
+
bun create cascade my-app --install
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Behavior
|
|
73
|
+
|
|
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`.
|
|
77
|
+
|
|
78
|
+
## Package
|
|
79
|
+
|
|
80
|
+
Published on npm as `create-cascade`.
|
package/index.js
CHANGED
|
@@ -1,23 +1,85 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs"
|
|
4
|
+
import { basename, resolve } from "node:path"
|
|
5
5
|
import process from "node:process"
|
|
6
|
-
import {
|
|
6
|
+
import { spawnSync } from "node:child_process"
|
|
7
|
+
import { createInterface } from "node:readline/promises"
|
|
8
|
+
import { emitKeypressEvents } from "node:readline"
|
|
7
9
|
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
+
const ANSI_RESET = "\x1b[0m"
|
|
11
|
+
const ANSI_BOLD = "\x1b[1m"
|
|
12
|
+
const ANSI_DIM = "\x1b[2m"
|
|
13
|
+
const ANSI_CYAN = "\x1b[36m"
|
|
14
|
+
|
|
15
|
+
const FRAMEWORKS = [
|
|
16
|
+
{ id: "core", label: "Core", description: "Vanilla Cascade API with renderables" },
|
|
17
|
+
{ id: "react", label: "React", description: "Cascade renderer with React components" },
|
|
18
|
+
{ id: "solid", label: "Solid", description: "Cascade renderer with SolidJS components" },
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
const STARTERS = {
|
|
22
|
+
core: [
|
|
23
|
+
{ id: "minimal", label: "Minimal", description: "Single welcome message" },
|
|
24
|
+
{ id: "counter", label: "Counter", description: "Live counter updated every second" },
|
|
25
|
+
{ id: "layout", label: "Layout", description: "Simple boxed layout starter" },
|
|
26
|
+
],
|
|
27
|
+
react: [
|
|
28
|
+
{ id: "minimal", label: "Minimal", description: "Render a single text node" },
|
|
29
|
+
{ id: "counter", label: "Counter", description: "React state with interval updates" },
|
|
30
|
+
{ id: "login", label: "Login", description: "Small interactive login form" },
|
|
31
|
+
],
|
|
32
|
+
solid: [
|
|
33
|
+
{ id: "minimal", label: "Minimal", description: "Render a single text node" },
|
|
34
|
+
{ id: "counter", label: "Counter", description: "Solid signal with interval updates" },
|
|
35
|
+
{ id: "input", label: "Input", description: "Basic input and submit interaction" },
|
|
36
|
+
],
|
|
37
|
+
}
|
|
10
38
|
|
|
11
39
|
function parseArgs(argv) {
|
|
12
40
|
const args = argv.slice(2)
|
|
13
41
|
const options = {
|
|
14
|
-
|
|
42
|
+
install: false,
|
|
43
|
+
start: false,
|
|
44
|
+
here: false,
|
|
45
|
+
framework: undefined,
|
|
46
|
+
starter: undefined,
|
|
47
|
+
help: false,
|
|
15
48
|
}
|
|
16
49
|
const positionals = []
|
|
17
50
|
|
|
18
|
-
for (
|
|
51
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
52
|
+
const arg = args[i]
|
|
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
|
+
}
|
|
19
63
|
if (arg === "--no-install") {
|
|
20
|
-
options.
|
|
64
|
+
options.install = false
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
if (arg === "--no-start") {
|
|
68
|
+
options.start = false
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
if (arg === "--here") {
|
|
72
|
+
options.here = true
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
if (arg === "--framework" || arg === "-f" || arg === "--template" || arg === "-t") {
|
|
76
|
+
options.framework = args[i + 1]
|
|
77
|
+
i += 1
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
if (arg === "--starter" || arg === "-s") {
|
|
81
|
+
options.starter = args[i + 1]
|
|
82
|
+
i += 1
|
|
21
83
|
continue
|
|
22
84
|
}
|
|
23
85
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -31,12 +93,29 @@ function parseArgs(argv) {
|
|
|
31
93
|
}
|
|
32
94
|
|
|
33
95
|
function printHelp() {
|
|
34
|
-
console.log("Usage: bun create cascade [project-name] [
|
|
96
|
+
console.log("Usage: bun create cascade [project-name] [options]")
|
|
97
|
+
console.log("")
|
|
98
|
+
console.log("Options:")
|
|
99
|
+
console.log(" -f, --framework <name> Framework: core, react, solid")
|
|
100
|
+
console.log(" -s, --starter <name> Starter preset for selected framework")
|
|
101
|
+
console.log(" --here Use current directory")
|
|
102
|
+
console.log(" --install Run bun install after scaffolding")
|
|
103
|
+
console.log(" --start Run bun install, then bun run dev")
|
|
104
|
+
console.log(" -h, --help Show help")
|
|
35
105
|
console.log("")
|
|
36
106
|
console.log("Examples:")
|
|
37
107
|
console.log(" bun create cascade")
|
|
38
108
|
console.log(" bun create cascade my-app")
|
|
39
|
-
console.log(" bun create cascade
|
|
109
|
+
console.log(" bun create cascade --here")
|
|
110
|
+
console.log(" bun create cascade my-app -f react -s counter")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function normalizePackageName(name) {
|
|
114
|
+
return name
|
|
115
|
+
.trim()
|
|
116
|
+
.toLowerCase()
|
|
117
|
+
.replace(/[^a-z0-9-_]+/g, "-")
|
|
118
|
+
.replace(/^-+|-+$/g, "") || "cascade-app"
|
|
40
119
|
}
|
|
41
120
|
|
|
42
121
|
function ensureDirectoryIsEmpty(targetDir) {
|
|
@@ -49,57 +128,526 @@ function ensureDirectoryIsEmpty(targetDir) {
|
|
|
49
128
|
}
|
|
50
129
|
}
|
|
51
130
|
|
|
52
|
-
function
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
131
|
+
function promptLine(rl, label) {
|
|
132
|
+
return rl.question(label)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function selectOption(rl, label, options) {
|
|
136
|
+
if (!process.stdin.isTTY) {
|
|
137
|
+
return options[0].id
|
|
138
|
+
}
|
|
139
|
+
|
|
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")
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
stdout.write(`${ANSI_BOLD}${label}${ANSI_RESET}\n`)
|
|
154
|
+
|
|
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
|
+
}
|
|
163
|
+
|
|
164
|
+
stdout.write(`${ANSI_DIM}Use Up/Down arrows and Enter${ANSI_RESET}\n`)
|
|
165
|
+
renderedOnce = true
|
|
166
|
+
}
|
|
167
|
+
|
|
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
|
+
}
|
|
176
|
+
|
|
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
|
+
}
|
|
206
|
+
|
|
207
|
+
emitKeypressEvents(stdin)
|
|
208
|
+
stdin.setRawMode(true)
|
|
209
|
+
stdin.resume()
|
|
210
|
+
stdin.on("keypress", onKeyPress)
|
|
211
|
+
render()
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function resolveFramework(rl, frameworkArg) {
|
|
216
|
+
if (frameworkArg) {
|
|
217
|
+
const found = FRAMEWORKS.find((entry) => entry.id === frameworkArg)
|
|
218
|
+
if (!found) {
|
|
219
|
+
throw new Error(`Unknown framework: ${frameworkArg}. Use core, react, or solid.`)
|
|
220
|
+
}
|
|
221
|
+
return found.id
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!process.stdin.isTTY) {
|
|
225
|
+
return "core"
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return selectOption(rl, "Choose a framework:", FRAMEWORKS)
|
|
58
229
|
}
|
|
59
230
|
|
|
60
|
-
function
|
|
61
|
-
const
|
|
62
|
-
cpSync(templateDir, targetDir, { recursive: true })
|
|
231
|
+
async function resolveStarter(rl, framework, starterArg) {
|
|
232
|
+
const choices = STARTERS[framework]
|
|
63
233
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
234
|
+
if (starterArg) {
|
|
235
|
+
const found = choices.find((entry) => entry.id === starterArg)
|
|
236
|
+
if (!found) {
|
|
237
|
+
const allowed = choices.map((entry) => entry.id).join(", ")
|
|
238
|
+
throw new Error(`Unknown starter '${starterArg}' for ${framework}. Allowed: ${allowed}.`)
|
|
239
|
+
}
|
|
240
|
+
return found.id
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!process.stdin.isTTY) {
|
|
244
|
+
return choices[0].id
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return selectOption(rl, `Choose a starter for ${framework}:`, choices)
|
|
69
248
|
}
|
|
70
249
|
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
|
|
290
|
+
function getPackageJson(projectName, framework) {
|
|
291
|
+
const dependencies = {
|
|
292
|
+
"@cascadetui/core": "latest",
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (framework === "react") {
|
|
296
|
+
dependencies["@cascadetui/react"] = "latest"
|
|
297
|
+
dependencies.react = "latest"
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (framework === "solid") {
|
|
301
|
+
dependencies["@cascadetui/solid"] = "latest"
|
|
302
|
+
dependencies["solid-js"] = "latest"
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const isJsx = framework !== "core"
|
|
306
|
+
const entry = isJsx ? "src/index.tsx" : "src/index.ts"
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
name: normalizePackageName(projectName),
|
|
310
|
+
version: "0.0.1",
|
|
311
|
+
private: true,
|
|
312
|
+
type: "module",
|
|
313
|
+
scripts: {
|
|
314
|
+
dev: `bun run ${entry}`,
|
|
315
|
+
},
|
|
316
|
+
dependencies,
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function getTsConfig(framework) {
|
|
321
|
+
const base = {
|
|
322
|
+
target: "ESNext",
|
|
323
|
+
module: "ESNext",
|
|
324
|
+
moduleResolution: "Bundler",
|
|
325
|
+
strict: true,
|
|
326
|
+
skipLibCheck: true,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (framework === "react") {
|
|
330
|
+
return {
|
|
331
|
+
compilerOptions: {
|
|
332
|
+
...base,
|
|
333
|
+
lib: ["ESNext", "DOM"],
|
|
334
|
+
jsx: "react-jsx",
|
|
335
|
+
jsxImportSource: "@cascadetui/react",
|
|
336
|
+
},
|
|
337
|
+
include: ["src"],
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (framework === "solid") {
|
|
342
|
+
return {
|
|
343
|
+
compilerOptions: {
|
|
344
|
+
...base,
|
|
345
|
+
jsx: "preserve",
|
|
346
|
+
jsxImportSource: "@cascadetui/solid",
|
|
347
|
+
},
|
|
348
|
+
include: ["src"],
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
compilerOptions: base,
|
|
354
|
+
include: ["src"],
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function getSource(framework, starter) {
|
|
359
|
+
const sources = {
|
|
360
|
+
core: {
|
|
361
|
+
minimal: `import { TextRenderable, createCliRenderer } from "@cascadetui/core"
|
|
362
|
+
|
|
363
|
+
const renderer = await createCliRenderer({ exitOnCtrlC: true })
|
|
364
|
+
|
|
365
|
+
const text = new TextRenderable(renderer, {
|
|
366
|
+
content: "Hello from Cascade",
|
|
367
|
+
margin: 2,
|
|
368
|
+
fg: "#00ff99",
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
renderer.root.add(text)
|
|
372
|
+
`,
|
|
373
|
+
counter: `import { TextRenderable, createCliRenderer } from "@cascadetui/core"
|
|
374
|
+
|
|
375
|
+
const renderer = await createCliRenderer({ exitOnCtrlC: true })
|
|
376
|
+
let count = 0
|
|
377
|
+
|
|
378
|
+
const text = new TextRenderable(renderer, {
|
|
379
|
+
content: "Count: 0",
|
|
380
|
+
margin: 2,
|
|
381
|
+
fg: "#00ff99",
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
renderer.root.add(text)
|
|
385
|
+
|
|
386
|
+
setInterval(() => {
|
|
387
|
+
count += 1
|
|
388
|
+
text.content = \`Count: \${count}\`
|
|
389
|
+
}, 1000)
|
|
390
|
+
`,
|
|
391
|
+
layout: `import { BoxRenderable, TextRenderable, createCliRenderer } from "@cascadetui/core"
|
|
392
|
+
|
|
393
|
+
const renderer = await createCliRenderer({ exitOnCtrlC: true })
|
|
394
|
+
|
|
395
|
+
const container = new BoxRenderable(renderer, {
|
|
396
|
+
border: true,
|
|
397
|
+
borderStyle: "single",
|
|
398
|
+
padding: 1,
|
|
399
|
+
margin: 1,
|
|
400
|
+
width: 50,
|
|
401
|
+
height: 9,
|
|
402
|
+
flexDirection: "column",
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
const title = new TextRenderable(renderer, {
|
|
406
|
+
content: "Cascade Core Starter",
|
|
407
|
+
fg: "#00ff99",
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
const subtitle = new TextRenderable(renderer, {
|
|
411
|
+
content: "Press Ctrl+C to exit",
|
|
412
|
+
marginTop: 1,
|
|
413
|
+
fg: "#cccccc",
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
container.add(title)
|
|
417
|
+
container.add(subtitle)
|
|
418
|
+
renderer.root.add(container)
|
|
419
|
+
`,
|
|
420
|
+
},
|
|
421
|
+
react: {
|
|
422
|
+
minimal: `import { createCliRenderer } from "@cascadetui/core"
|
|
423
|
+
import { createRoot } from "@cascadetui/react"
|
|
424
|
+
|
|
425
|
+
function App() {
|
|
426
|
+
return <text content="Hello from Cascade + React" fg="#00ff99" />
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const renderer = await createCliRenderer({ exitOnCtrlC: true })
|
|
430
|
+
createRoot(renderer).render(<App />)
|
|
431
|
+
`,
|
|
432
|
+
counter: `import { createCliRenderer } from "@cascadetui/core"
|
|
433
|
+
import { createRoot } from "@cascadetui/react"
|
|
434
|
+
import { useEffect, useState } from "react"
|
|
435
|
+
|
|
436
|
+
function App() {
|
|
437
|
+
const [count, setCount] = useState(0)
|
|
438
|
+
|
|
439
|
+
useEffect(() => {
|
|
440
|
+
const timer = setInterval(() => setCount((value) => value + 1), 1000)
|
|
441
|
+
return () => clearInterval(timer)
|
|
442
|
+
}, [])
|
|
443
|
+
|
|
444
|
+
return (
|
|
445
|
+
<box style={{ border: true, padding: 1, margin: 1 }}>
|
|
446
|
+
<text content={\`React counter: \${count}\`} fg="#00ff99" />
|
|
447
|
+
</box>
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const renderer = await createCliRenderer({ exitOnCtrlC: true })
|
|
452
|
+
createRoot(renderer).render(<App />)
|
|
453
|
+
`,
|
|
454
|
+
login: `import { createCliRenderer } from "@cascadetui/core"
|
|
455
|
+
import { createRoot, useKeyboard } from "@cascadetui/react"
|
|
456
|
+
import { useState } from "react"
|
|
457
|
+
|
|
458
|
+
function App() {
|
|
459
|
+
const [username, setUsername] = useState("")
|
|
460
|
+
const [password, setPassword] = useState("")
|
|
461
|
+
const [focused, setFocused] = useState("username")
|
|
462
|
+
const [status, setStatus] = useState("idle")
|
|
463
|
+
|
|
464
|
+
useKeyboard((key) => {
|
|
465
|
+
if (key.name === "tab") {
|
|
466
|
+
setFocused((value) => (value === "username" ? "password" : "username"))
|
|
467
|
+
}
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
const submit = () => {
|
|
471
|
+
if (username === "admin" && password === "secret") {
|
|
472
|
+
setStatus("success")
|
|
76
473
|
return
|
|
77
474
|
}
|
|
475
|
+
setStatus("invalid")
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return (
|
|
479
|
+
<box style={{ padding: 2, flexDirection: "column" }}>
|
|
480
|
+
<text content="Cascade Login" fg="#00ff99" />
|
|
481
|
+
<box title="Username" style={{ border: true, width: 40, height: 3, marginTop: 1 }}>
|
|
482
|
+
<input focused={focused === "username"} placeholder="admin" onInput={setUsername} onSubmit={submit} />
|
|
483
|
+
</box>
|
|
484
|
+
<box title="Password" style={{ border: true, width: 40, height: 3, marginTop: 1 }}>
|
|
485
|
+
<input focused={focused === "password"} placeholder="secret" onInput={setPassword} onSubmit={submit} />
|
|
486
|
+
</box>
|
|
487
|
+
<text content={\`Status: \${status}\`} marginTop={1} fg={status === "success" ? "green" : "yellow"} />
|
|
488
|
+
</box>
|
|
489
|
+
)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const renderer = await createCliRenderer({ exitOnCtrlC: true })
|
|
493
|
+
createRoot(renderer).render(<App />)
|
|
494
|
+
`,
|
|
495
|
+
},
|
|
496
|
+
solid: {
|
|
497
|
+
minimal: `import { render } from "@cascadetui/solid"
|
|
78
498
|
|
|
79
|
-
|
|
80
|
-
const targetDir = resolve(process.cwd(), projectName)
|
|
499
|
+
const App = () => <text content="Hello from Cascade + Solid" fg="#00ff99" />
|
|
81
500
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
501
|
+
render(App, { exitOnCtrlC: true })
|
|
502
|
+
`,
|
|
503
|
+
counter: `import { render } from "@cascadetui/solid"
|
|
504
|
+
import { createSignal, onCleanup } from "solid-js"
|
|
505
|
+
|
|
506
|
+
const App = () => {
|
|
507
|
+
const [count, setCount] = createSignal(0)
|
|
508
|
+
const timer = setInterval(() => setCount((value) => value + 1), 1000)
|
|
509
|
+
onCleanup(() => clearInterval(timer))
|
|
510
|
+
|
|
511
|
+
return (
|
|
512
|
+
<box style={{ border: true, padding: 1, margin: 1 }}>
|
|
513
|
+
<text content={\`Solid counter: \${count()}\`} fg="#00ff99" />
|
|
514
|
+
</box>
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
render(App, { exitOnCtrlC: true })
|
|
519
|
+
`,
|
|
520
|
+
input: `import { render } from "@cascadetui/solid"
|
|
521
|
+
import { createSignal } from "solid-js"
|
|
522
|
+
|
|
523
|
+
const App = () => {
|
|
524
|
+
const [value, setValue] = createSignal("")
|
|
525
|
+
const [submitted, setSubmitted] = createSignal("")
|
|
526
|
+
|
|
527
|
+
return (
|
|
528
|
+
<box style={{ padding: 2, flexDirection: "column" }}>
|
|
529
|
+
<text content="Cascade Solid Input" fg="#00ff99" />
|
|
530
|
+
<box title="Message" style={{ border: true, width: 40, height: 3, marginTop: 1 }}>
|
|
531
|
+
<input
|
|
532
|
+
focused
|
|
533
|
+
placeholder="Type something..."
|
|
534
|
+
onInput={setValue}
|
|
535
|
+
onSubmit={(nextValue) => setSubmitted(nextValue)}
|
|
536
|
+
/>
|
|
537
|
+
</box>
|
|
538
|
+
<text content={\`Current: \${value()}\`} marginTop={1} />
|
|
539
|
+
<text content={\`Submitted: \${submitted() || "-"}\`} />
|
|
540
|
+
</box>
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
render(App, { exitOnCtrlC: true })
|
|
545
|
+
`,
|
|
546
|
+
},
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return sources[framework][starter]
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function writeProject(targetDir, projectName, framework, starter) {
|
|
553
|
+
mkdirSync(targetDir, { recursive: true })
|
|
554
|
+
ensureDirectoryIsEmpty(targetDir)
|
|
555
|
+
mkdirSync(resolve(targetDir, "src"), { recursive: true })
|
|
556
|
+
|
|
557
|
+
const packageJson = getPackageJson(projectName, framework)
|
|
558
|
+
const tsconfig = getTsConfig(framework)
|
|
559
|
+
const isJsx = framework !== "core"
|
|
560
|
+
const fileName = isJsx ? "index.tsx" : "index.ts"
|
|
561
|
+
const source = getSource(framework, starter)
|
|
562
|
+
|
|
563
|
+
writeFileSync(resolve(targetDir, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`)
|
|
564
|
+
writeFileSync(resolve(targetDir, "tsconfig.json"), `${JSON.stringify(tsconfig, null, 2)}\n`)
|
|
565
|
+
writeFileSync(resolve(targetDir, "src", fileName), source)
|
|
566
|
+
|
|
567
|
+
if (framework === "solid") {
|
|
568
|
+
writeFileSync(resolve(targetDir, "bunfig.toml"), 'preload = ["@cascadetui/solid/preload"]\n')
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function runCommand(command, args, cwd) {
|
|
573
|
+
const result = spawnSync(command, args, {
|
|
574
|
+
cwd,
|
|
575
|
+
stdio: "inherit",
|
|
576
|
+
shell: process.platform === "win32",
|
|
577
|
+
})
|
|
578
|
+
if (result.status !== 0) {
|
|
579
|
+
throw new Error(`Command failed: ${command} ${args.join(" ")}`)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async function main() {
|
|
584
|
+
const { options, positionals } = parseArgs(process.argv)
|
|
585
|
+
if (options.help) {
|
|
586
|
+
printHelp()
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const rl = createInterface({
|
|
591
|
+
input: process.stdin,
|
|
592
|
+
output: process.stdout,
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
let framework
|
|
597
|
+
let projectName
|
|
598
|
+
let location
|
|
599
|
+
let starter
|
|
600
|
+
|
|
601
|
+
if (shouldRunInteractiveWizard(options, positionals)) {
|
|
602
|
+
framework = await resolveFramework(rl, undefined)
|
|
603
|
+
projectName = await resolveProjectName(rl, [], "cascade-app")
|
|
604
|
+
location = await resolveLocation(rl, { ...options, here: false }, [])
|
|
605
|
+
starter = await resolveStarter(rl, framework, undefined)
|
|
606
|
+
} else {
|
|
607
|
+
framework = await resolveFramework(rl, options.framework)
|
|
608
|
+
location = await resolveLocation(rl, options, positionals)
|
|
609
|
+
const defaultName = location === "here" ? basename(process.cwd()) : "cascade-app"
|
|
610
|
+
projectName = await resolveProjectName(rl, positionals, defaultName)
|
|
611
|
+
starter = await resolveStarter(rl, framework, options.starter)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const usingCurrentDirectory = location === "here"
|
|
615
|
+
const targetDir = usingCurrentDirectory ? process.cwd() : resolve(process.cwd(), projectName)
|
|
616
|
+
const packageNameSeed = projectName
|
|
617
|
+
|
|
618
|
+
writeProject(targetDir, packageNameSeed, framework, starter)
|
|
85
619
|
|
|
86
620
|
console.log("")
|
|
87
|
-
console.log(`Created project in ${targetDir}`)
|
|
621
|
+
console.log(`Created Cascade project in ${targetDir}`)
|
|
622
|
+
console.log(`Framework: ${framework}`)
|
|
623
|
+
console.log(`Starter: ${starter}`)
|
|
624
|
+
|
|
625
|
+
if (options.install) {
|
|
626
|
+
console.log("")
|
|
627
|
+
console.log("Installing dependencies with bun...")
|
|
628
|
+
runCommand("bun", ["install"], targetDir)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (options.start) {
|
|
632
|
+
console.log("")
|
|
633
|
+
console.log("Starting the project...")
|
|
634
|
+
runCommand("bun", ["run", "dev"], targetDir)
|
|
635
|
+
return
|
|
636
|
+
}
|
|
637
|
+
|
|
88
638
|
console.log("")
|
|
89
639
|
console.log("Next steps:")
|
|
90
|
-
if (
|
|
640
|
+
if (!usingCurrentDirectory) {
|
|
91
641
|
console.log(` cd ${projectName}`)
|
|
92
642
|
}
|
|
93
|
-
|
|
94
|
-
console.log(" bun install")
|
|
95
|
-
} else {
|
|
96
|
-
console.log(" bun install")
|
|
97
|
-
}
|
|
643
|
+
console.log(" bun install")
|
|
98
644
|
console.log(" bun run dev")
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
process.exit(1)
|
|
645
|
+
} finally {
|
|
646
|
+
rl.close()
|
|
102
647
|
}
|
|
103
648
|
}
|
|
104
649
|
|
|
105
|
-
main()
|
|
650
|
+
main().catch((error) => {
|
|
651
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
652
|
+
process.exit(1)
|
|
653
|
+
})
|
package/package.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-cascade",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Create a new Cascade TUI project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/kirosnn/cascade",
|
|
9
|
+
"url": "git+https://github.com/kirosnn/cascade.git",
|
|
10
10
|
"directory": "packages/create-cascade"
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
13
|
-
"create-cascade": "
|
|
13
|
+
"create-cascade": "index.js"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"index.js",
|
|
17
|
-
"
|
|
17
|
+
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
-
"publish": "bun scripts/publish.ts"
|
|
20
|
+
"publish:pkg": "bun scripts/publish.ts"
|
|
21
21
|
}
|
|
22
22
|
}
|
package/template/package.json
DELETED
package/template/src/index.ts
DELETED