create-raiko 0.0.0-stage → 1.0.0
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/LICENSE +21 -0
- package/README.md +21 -2
- package/index.mjs +141 -0
- package/package.json +12 -4
- package/prompt.mjs +110 -0
- package/templates-common.mjs +439 -0
- package/templates.mjs +632 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexandre Kohler
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-raiko
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Create a [Raiko.js](https://kwy404.github.io/Raiko.js/) app.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx create-raiko@latest my-app
|
|
7
|
+
# or
|
|
8
|
+
bun create raiko my-app
|
|
9
|
+
# or
|
|
10
|
+
pnpm create raiko my-app
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It asks for the frontend (Raiko Pages with Voodoo.js, React, Vue, Svelte, Solid, Preact, Angular), CSS (Monsoon, Tailwind, StyleX, UnoCSS), icons (glyphfox, Lucide, Heroicons), animations (Motion), database (Kastor, Prisma, Drizzle on SQLite or PostgreSQL), sign in (email and password, GitHub, Google, Discord, GitLab), two factor auth, email (Resend, Postmark, SendGrid, SMTP), translations (en, pt-BR, es, fr, de), admin roles with permissions per action, a cookie banner (cookieowl, LGPD or GDPR), an installable app (installkite) and the deploy target (Vercel, Cloudflare Workers, Docker, plain server).
|
|
14
|
+
|
|
15
|
+
Every answer is also a flag, for scripts and CI:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx create-raiko@latest my-app --yes --frontend react --css tailwind --orm kastor --db sqlite \
|
|
19
|
+
--auth credentials,github --twofa true --admin true --roles editor --deploy vercel
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
A random `AUTH_SECRET` is written to `.env` (never to code). MIT License.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// create-raiko: bun create raiko | npm create raiko@latest | pnpm create raiko
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { dirname, join, resolve, basename } from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { randomBytes } from 'node:crypto'
|
|
7
|
+
import { select, multiselect, text, confirm, spin, col } from './prompt.mjs'
|
|
8
|
+
import { generate, FRONTENDS, CSS, ICONS, run } from './templates.mjs'
|
|
9
|
+
|
|
10
|
+
const argv = process.argv.slice(2)
|
|
11
|
+
const flag = (n) => {
|
|
12
|
+
const i = argv.indexOf(`--${n}`)
|
|
13
|
+
if (i === -1) return undefined
|
|
14
|
+
const v = argv[i + 1]
|
|
15
|
+
return v === undefined || v.startsWith('--') ? true : v
|
|
16
|
+
}
|
|
17
|
+
const yes = argv.includes('--yes') || argv.includes('-y')
|
|
18
|
+
const ask = async (name, fn, def) => (flag(name) !== undefined ? parse(flag(name)) : yes ? def : fn())
|
|
19
|
+
const parse = (v) => (v === 'true' ? true : v === 'false' ? false : v)
|
|
20
|
+
const list = (v) => (Array.isArray(v) ? v : v === 'none' || v === '' ? [] : String(v).split(','))
|
|
21
|
+
|
|
22
|
+
const ua = process.env.npm_config_user_agent ?? ''
|
|
23
|
+
const detectedPm = ua.startsWith('bun') || process.versions.bun ? 'bun' : ua.startsWith('pnpm') ? 'pnpm' : ua.startsWith('yarn') ? 'yarn' : 'npm'
|
|
24
|
+
|
|
25
|
+
const ACTIONS = ['list', 'read', 'create', 'update', 'delete']
|
|
26
|
+
const RESOURCES = ['users', 'posts']
|
|
27
|
+
const ROLE_PRESETS = {
|
|
28
|
+
admin: { users: '*', posts: '*' },
|
|
29
|
+
editor: { users: ['read'], posts: ['list', 'read', 'create', 'update'] },
|
|
30
|
+
support: { users: ['read', 'update'], posts: ['read'] },
|
|
31
|
+
viewer: { users: [], posts: ['list', 'read'] },
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(`
|
|
35
|
+
${col.y(' /\\_/\\ ')} ${col.b('raiko')}${col.d('.js')}
|
|
36
|
+
${col.y(' ( o.o ) ')} ${col.d('backend and frontend in one file tree')}
|
|
37
|
+
${col.y(' > ^ < ')} ${col.d('every runtime. strikes first.')}
|
|
38
|
+
`)
|
|
39
|
+
|
|
40
|
+
const positional = argv.find((a, i) => !a.startsWith('-') && !(argv[i - 1] ?? '').startsWith('--'))
|
|
41
|
+
const name = positional ?? (yes ? 'raiko-app' : await text('Project name', 'raiko-app'))
|
|
42
|
+
// npm package name rules: the name also lands in generated code, so nothing else is allowed
|
|
43
|
+
if (!/^[a-z0-9][a-z0-9._-]{0,213}$/.test(basename(name))) {
|
|
44
|
+
console.error(`\n ${col.r('error[RK0803]')}: "${basename(name).replace(/[^\x20-\x7e]/g, '?')}" is not a valid project name\n = help: lowercase letters, digits, dot, dash and underscore, like my-app\n`)
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
const dir = resolve(name)
|
|
48
|
+
if (existsSync(dir) && readdirSync(dir).length && !argv.includes('--force')) {
|
|
49
|
+
console.error(`\n ${col.r('error[RK0801]')}: folder ${name} is not empty\n = help: pick another name or pass --force\n`)
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const template = await ask('template', () => select('Template', [{ value: 'full', label: 'Full', hint: 'pages, API, chat over WebSocket, everything you pick below' }, { value: 'minimal', label: 'Minimal', hint: 'one page and one API route' }]), 'full')
|
|
54
|
+
const frontend = await ask('frontend', () => select('Frontend', FRONTENDS), 'raiko')
|
|
55
|
+
const css = await ask('css', () => select('CSS', CSS.filter((c) => !c.only || c.only.includes(frontend))), 'monsoon')
|
|
56
|
+
if (css === 'stylex' && frontend !== 'react') {
|
|
57
|
+
console.error(` ${col.r('error[RK0802]')}: StyleX needs the React frontend\n = help: pick --frontend react, or another CSS option`)
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
const icons = await ask('icons', () => select('Icons', ICONS.filter((c) => !c.only || c.only.includes(frontend))), 'glyphfox')
|
|
61
|
+
const motion = await ask('motion', () => confirm('Animations with Motion (formerly Framer Motion)?', false), false)
|
|
62
|
+
const orm = await ask('orm', () => select('Database ORM', [{ value: 'kastor', label: 'Kastor', hint: 'TypeScript ORM, no codegen' }, { value: 'prisma', label: 'Prisma' }, { value: 'drizzle', label: 'Drizzle' }, { value: 'none', label: 'No database' }]), 'none')
|
|
63
|
+
const db = orm === 'none' ? 'none' : await ask('db', () => select('Database', [{ value: 'sqlite', label: 'SQLite', hint: 'a file, no server needed' }, { value: 'postgres', label: 'PostgreSQL' }]), 'sqlite')
|
|
64
|
+
const auth = list(await ask('auth', () => multiselect('Sign in methods', [{ value: 'credentials', label: 'Email and password' }, { value: 'github', label: 'GitHub' }, { value: 'google', label: 'Google' }, { value: 'discord', label: 'Discord' }, { value: 'gitlab', label: 'GitLab' }], ['credentials']), []))
|
|
65
|
+
const twofa = auth.length ? await ask('twofa', () => confirm('Two factor auth with Google Authenticator (TOTP)?', true), false) : false
|
|
66
|
+
const email = await ask('email', () => select('Email provider', [{ value: 'resend', label: 'Resend' }, { value: 'postmark', label: 'Postmark' }, { value: 'sendgrid', label: 'SendGrid' }, { value: 'smtp', label: 'SMTP', hint: 'any server, via nodemailer' }, { value: 'none', label: 'None for now', hint: 'emails print in the terminal' }]), 'none')
|
|
67
|
+
const i18n = await ask('i18n', () => confirm('Translations (en, pt-BR, es, fr, de)?', false), false)
|
|
68
|
+
const admin = orm === 'none' ? false : await ask('admin', () => confirm('Admin panel at /admin?', true), true)
|
|
69
|
+
|
|
70
|
+
let roles = { admin: ROLE_PRESETS.admin }
|
|
71
|
+
if (admin) {
|
|
72
|
+
const picked = list(await ask('roles', () => multiselect('Admin roles (admin always has full access)', [{ value: 'editor', label: 'editor', hint: 'manage posts, read users' }, { value: 'support', label: 'support', hint: 'read and fix users, cannot list or delete' }, { value: 'viewer', label: 'viewer', hint: 'read only' }], ['editor']), 'editor'))
|
|
73
|
+
for (const r of picked) if (ROLE_PRESETS[r]) roles[r] = ROLE_PRESETS[r]
|
|
74
|
+
}
|
|
75
|
+
const cookies = await ask('cookies', () => select('Cookie consent banner (cookieowl)', [{ value: 'none', label: 'None' }, { value: 'lgpd', label: 'LGPD', hint: 'Brazil, Portuguese texts' }, { value: 'gdpr', label: 'GDPR', hint: 'Europe, visitor language' }]), 'none')
|
|
76
|
+
const pwa = await ask('pwa', () => confirm('Installable app with offline support (installkite)?', false), false)
|
|
77
|
+
const deploy = await ask('deploy', () => select('Deploy target', [{ value: 'vercel', label: 'Vercel' }, { value: 'cloudflare', label: 'Cloudflare Workers' }, { value: 'docker', label: 'Docker', hint: 'Fly, Railway, Render, any VPS' }, { value: 'node', label: 'Plain server', hint: 'bun or node on your machine' }]), 'docker')
|
|
78
|
+
const pm = await ask('pm', () => select('Package manager', [{ value: detectedPm, label: detectedPm, hint: 'detected' }, ...['bun', 'npm', 'pnpm', 'yarn'].filter((p) => p !== detectedPm).map((p) => ({ value: p, label: p }))]), detectedPm)
|
|
79
|
+
const install = await ask('install', () => confirm('Install dependencies now?', true), !argv.includes('--no-install'))
|
|
80
|
+
const git = await ask('git', () => confirm('Initialize a git repository?', true), !argv.includes('--no-git'))
|
|
81
|
+
|
|
82
|
+
const o = { name: basename(dir), template, frontend, css, icons, motion, orm, db, auth, twofa, email, i18n, admin, roles, cookies, pwa, deploy, pm }
|
|
83
|
+
if (flag('dry') !== undefined) {
|
|
84
|
+
console.log(JSON.stringify(o, null, 2))
|
|
85
|
+
process.exit(0)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const files = generate(o)
|
|
89
|
+
await spin(`writing ${Object.keys(files).length} files`, async () => {
|
|
90
|
+
for (const [p, content] of Object.entries(files)) {
|
|
91
|
+
const full = join(dir, p)
|
|
92
|
+
mkdirSync(dirname(full), { recursive: true })
|
|
93
|
+
writeFileSync(full, content)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// a real random secret, only in the local .env (gitignored, dockerignored), never in code
|
|
98
|
+
writeFileSync(join(dir, '.env'), files['.env.example'].replace(/^AUTH_SECRET=$/m, `AUTH_SECRET=${randomBytes(48).toString('base64url')}`))
|
|
99
|
+
|
|
100
|
+
if (admin) {
|
|
101
|
+
console.log(`\n ${col.b('Admin permissions')} ${col.d('(edit them in raiko.config.ts)')}\n`)
|
|
102
|
+
const w = Math.max(...Object.keys(roles).map((r) => r.length), 4)
|
|
103
|
+
console.log(` ${'role'.padEnd(w)} ${RESOURCES.map((r) => r.padEnd(34)).join('')}`)
|
|
104
|
+
for (const [role, res] of Object.entries(roles)) {
|
|
105
|
+
const cells = RESOURCES.map((r) => {
|
|
106
|
+
const a = res[r] ?? []
|
|
107
|
+
const s = a === '*' ? 'all' : ACTIONS.map((x) => (a.includes(x) ? x : col.d('-'.repeat(x.length)))).join(' ')
|
|
108
|
+
return s + ' '.repeat(Math.max(1, 34 - (a === '*' ? 3 : ACTIONS.join(' ').length)))
|
|
109
|
+
})
|
|
110
|
+
console.log(` ${col.y(role.padEnd(w))} ${cells.join('')}`)
|
|
111
|
+
}
|
|
112
|
+
console.log(`\n ${col.d('a role can read or update a row without being able to list the table')}`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (install) {
|
|
116
|
+
const r = await spin(`installing with ${pm}`, async () => spawnSync(pm, ['install'], { cwd: dir, stdio: 'pipe', shell: process.platform === 'win32' }))
|
|
117
|
+
if (r.status) {
|
|
118
|
+
console.log(col.r(String(r.stderr ?? '').slice(-2000)))
|
|
119
|
+
console.log(` ${col.r('install failed')}: run ${col.y(`cd ${name} && ${pm} install`)} to see why`)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (pwa && install) {
|
|
123
|
+
await spin('creating PWA files with installkite', async () =>
|
|
124
|
+
spawnSync(pm === 'bun' ? 'bunx' : 'npx', ['installkite', 'init', '--yes'], { cwd: join(dir, frontend === 'raiko' ? 'public' : 'web/public'), stdio: 'pipe', shell: process.platform === 'win32' }),
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
if (git && spawnSync('git', ['--version'], { stdio: 'ignore' }).status === 0) {
|
|
128
|
+
spawnSync('git', ['init', '-q', '-b', 'main'], { cwd: dir })
|
|
129
|
+
spawnSync('git', ['add', '-A'], { cwd: dir })
|
|
130
|
+
spawnSync('git', ['-c', 'user.name=raiko', '-c', 'user.email=raiko@localhost', 'commit', '-q', '-m', 'Initial commit from create-raiko'], { cwd: dir })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`
|
|
134
|
+
${col.g('done.')} next:
|
|
135
|
+
|
|
136
|
+
cd ${name}
|
|
137
|
+
${install ? '' : `${pm} install\n `}${run(o, 'dev')}
|
|
138
|
+
${orm !== 'none' ? ` ${run(o, 'db:migrate')} ${col.d('create the tables')}\n` : ''}
|
|
139
|
+
${col.d('docs')} https://kwy404.github.io/Raiko.js/docs/
|
|
140
|
+
${col.d('deploy')} ${run(o, 'deploy')}
|
|
141
|
+
`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-raiko",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
}
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Create a Raiko.js app: pick your frontend, CSS, database, auth, email, i18n and deploy target.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "create-raiko": "./index.mjs" },
|
|
7
|
+
"files": ["index.mjs", "prompt.mjs", "templates.mjs", "templates-common.mjs", "README.md"],
|
|
8
|
+
"engines": { "node": ">=20" },
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Alexandre Kohler (https://github.com/kwy404)",
|
|
11
|
+
"homepage": "https://kwy404.github.io/Raiko.js/",
|
|
12
|
+
"repository": { "type": "git", "url": "git+https://github.com/kwy404/Raiko.js.git", "directory": "packages/create-raiko" },
|
|
13
|
+
"keywords": ["raiko", "create", "scaffold", "template", "fullstack", "typescript"]
|
|
14
|
+
}
|
package/prompt.mjs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Tiny interactive prompts with arrow keys, no dependencies.
|
|
2
|
+
import readline from 'node:readline'
|
|
3
|
+
|
|
4
|
+
const tty = process.stdin.isTTY && process.stdout.isTTY
|
|
5
|
+
const esc = (n) => `\x1b[${n}m`
|
|
6
|
+
export const col = { y: (s) => `${esc(33)}${s}${esc(0)}`, d: (s) => `${esc(2)}${s}${esc(0)}`, g: (s) => `${esc(32)}${s}${esc(0)}`, b: (s) => `${esc(1)}${s}${esc(0)}`, r: (s) => `${esc(31)}${s}${esc(0)}`, c: (s) => `${esc(36)}${s}${esc(0)}` }
|
|
7
|
+
|
|
8
|
+
function keys(onKey) {
|
|
9
|
+
readline.emitKeypressEvents(process.stdin)
|
|
10
|
+
process.stdin.setRawMode(true)
|
|
11
|
+
process.stdin.resume()
|
|
12
|
+
const h = (_, k) => {
|
|
13
|
+
if (k?.ctrl && k.name === 'c') {
|
|
14
|
+
process.stdout.write('\x1b[?25h\n')
|
|
15
|
+
process.exit(130)
|
|
16
|
+
}
|
|
17
|
+
onKey(k ?? {})
|
|
18
|
+
}
|
|
19
|
+
process.stdin.on('keypress', h)
|
|
20
|
+
return () => {
|
|
21
|
+
process.stdin.off('keypress', h)
|
|
22
|
+
process.stdin.setRawMode(false)
|
|
23
|
+
process.stdin.pause()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Single choice. options: [{ value, label, hint }] */
|
|
28
|
+
export function select(question, options, initial = 0) {
|
|
29
|
+
if (!tty) return Promise.resolve(options[initial].value)
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
let i = initial
|
|
32
|
+
const draw = (first) => {
|
|
33
|
+
if (!first) process.stdout.write(`\x1b[${options.length}A`)
|
|
34
|
+
for (const [n, o] of options.entries())
|
|
35
|
+
process.stdout.write(`\x1b[2K ${n === i ? col.y('>') : ' '} ${n === i ? col.b(o.label) : o.label}${o.hint ? ' ' + col.d(o.hint) : ''}\n`)
|
|
36
|
+
}
|
|
37
|
+
process.stdout.write(`\x1b[?25l${col.c('?')} ${col.b(question)} ${col.d('(arrows, enter)')}\n`)
|
|
38
|
+
draw(true)
|
|
39
|
+
const stop = keys((k) => {
|
|
40
|
+
if (k.name === 'up') i = (i - 1 + options.length) % options.length
|
|
41
|
+
else if (k.name === 'down' || k.name === 'tab') i = (i + 1) % options.length
|
|
42
|
+
else if (k.name === 'return') {
|
|
43
|
+
stop()
|
|
44
|
+
process.stdout.write(`\x1b[${options.length + 1}A\x1b[0J${col.g('+')} ${question} ${col.y(options[i].label)}\n\x1b[?25h`)
|
|
45
|
+
return resolve(options[i].value)
|
|
46
|
+
} else return
|
|
47
|
+
draw(false)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Multiple choice with space. */
|
|
53
|
+
export function multiselect(question, options, initial = []) {
|
|
54
|
+
if (!tty) return Promise.resolve(initial)
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
let i = 0
|
|
57
|
+
const on = new Set(initial)
|
|
58
|
+
const draw = (first) => {
|
|
59
|
+
if (!first) process.stdout.write(`\x1b[${options.length}A`)
|
|
60
|
+
for (const [n, o] of options.entries())
|
|
61
|
+
process.stdout.write(`\x1b[2K ${n === i ? col.y('>') : ' '} ${on.has(o.value) ? col.g('[x]') : col.d('[ ]')} ${o.label}${o.hint ? ' ' + col.d(o.hint) : ''}\n`)
|
|
62
|
+
}
|
|
63
|
+
process.stdout.write(`\x1b[?25l${col.c('?')} ${col.b(question)} ${col.d('(space to toggle, enter)')}\n`)
|
|
64
|
+
draw(true)
|
|
65
|
+
const stop = keys((k) => {
|
|
66
|
+
if (k.name === 'up') i = (i - 1 + options.length) % options.length
|
|
67
|
+
else if (k.name === 'down') i = (i + 1) % options.length
|
|
68
|
+
else if (k.name === 'space') on.has(options[i].value) ? on.delete(options[i].value) : on.add(options[i].value)
|
|
69
|
+
else if (k.name === 'return') {
|
|
70
|
+
stop()
|
|
71
|
+
const picked = options.filter((o) => on.has(o.value))
|
|
72
|
+
process.stdout.write(`\x1b[${options.length + 1}A\x1b[0J${col.g('+')} ${question} ${col.y(picked.map((o) => o.label).join(', ') || 'none')}\n\x1b[?25h`)
|
|
73
|
+
return resolve(picked.map((o) => o.value))
|
|
74
|
+
} else return
|
|
75
|
+
draw(false)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function text(question, initial = '') {
|
|
81
|
+
if (!tty) return Promise.resolve(initial)
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
84
|
+
rl.question(`${col.c('?')} ${col.b(question)} ${col.d(`(${initial})`)} `, (a) => {
|
|
85
|
+
rl.close()
|
|
86
|
+
resolve(a.trim() || initial)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const confirm = (question, initial = true) =>
|
|
92
|
+
select(question, initial ? [{ value: true, label: 'Yes' }, { value: false, label: 'No' }] : [{ value: false, label: 'No' }, { value: true, label: 'Yes' }])
|
|
93
|
+
|
|
94
|
+
/** Little animated mascot while something runs. */
|
|
95
|
+
export async function spin(label, fn) {
|
|
96
|
+
if (!tty) return fn()
|
|
97
|
+
const frames = ['(=^.^=) ', '(=^.^=)~ ', '(=^-^=)~~', '(=^.^=) ~']
|
|
98
|
+
let n = 0
|
|
99
|
+
const t = setInterval(() => process.stdout.write(`\r\x1b[2K ${col.y(frames[n++ % frames.length])} ${label}`), 120)
|
|
100
|
+
try {
|
|
101
|
+
const r = await fn()
|
|
102
|
+
clearInterval(t)
|
|
103
|
+
process.stdout.write(`\r\x1b[2K ${col.g('ok')} ${label}\n`)
|
|
104
|
+
return r
|
|
105
|
+
} catch (e) {
|
|
106
|
+
clearInterval(t)
|
|
107
|
+
process.stdout.write(`\r\x1b[2K ${col.r('failed')} ${label}\n`)
|
|
108
|
+
throw e
|
|
109
|
+
}
|
|
110
|
+
}
|