create-murasaki 0.0.0 → 0.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/index.mjs +166 -5
- package/package.json +1 -1
- package/templates/default/package.json +18 -0
- package/templates/default/src/app.tsx +11 -0
- package/templates/default/src/layout.tsx +54 -0
package/index.mjs
CHANGED
|
@@ -1,11 +1,172 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// create-murasaki — Scaffolder for Murasaki apps.
|
|
3
|
-
//
|
|
3
|
+
// Usage: npm create murasaki@latest my-app
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { cp, mkdir, readFile, writeFile, stat } from 'node:fs/promises'
|
|
6
|
+
import { existsSync } from 'node:fs'
|
|
7
|
+
import { dirname, join, resolve } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import { createInterface } from 'node:readline/promises'
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
// ── ANSI truecolor (Oomurasaki palette) ────────────────────────────────
|
|
12
|
+
const BRIGHT = '\x1b[38;2;168;85;247m'
|
|
13
|
+
const DEEP = '\x1b[38;2;91;33;182m'
|
|
14
|
+
const CREAM = '\x1b[38;2;250;245;232m'
|
|
15
|
+
const DARK = '\x1b[38;2;59;7;100m'
|
|
16
|
+
const DIM = '\x1b[38;2;136;136;153m'
|
|
17
|
+
const GREEN = '\x1b[38;2;76;175;80m'
|
|
18
|
+
const RED = '\x1b[38;2;239;68;68m'
|
|
19
|
+
const BOLD = '\x1b[1m'
|
|
20
|
+
const RESET = '\x1b[0m'
|
|
21
|
+
|
|
22
|
+
const BG_BRIGHT = '\x1b[48;2;168;85;247m'
|
|
23
|
+
const BG_DEEP = '\x1b[48;2;91;33;182m'
|
|
24
|
+
const BG_CREAM = '\x1b[48;2;250;245;232m'
|
|
25
|
+
const BG_DARK = '\x1b[48;2;59;7;100m'
|
|
26
|
+
|
|
27
|
+
const noColor = process.env.NO_COLOR || !process.stdout.isTTY
|
|
28
|
+
const c = (code) => (noColor ? '' : code)
|
|
29
|
+
|
|
30
|
+
// ── H4 butterfly grid (19 col × 12 row) ────────────────────────────────
|
|
31
|
+
const GRID = [
|
|
32
|
+
'.....b.......b.....',
|
|
33
|
+
'......b.....b......',
|
|
34
|
+
'...bbbb.....bbbb...',
|
|
35
|
+
'..bbbbbb...bbbbbb..',
|
|
36
|
+
'.bbbbcbbb.bbbcbbbb.',
|
|
37
|
+
'.bbbbbbbb.bbbbbbbb.',
|
|
38
|
+
'..bbbbbbb.bbbbbbb..',
|
|
39
|
+
'...bbbbb...bbbbb...',
|
|
40
|
+
'...................',
|
|
41
|
+
'.....ddd...ddd.....',
|
|
42
|
+
'....ddddd.ddddd....',
|
|
43
|
+
'.....dddd.dddd.....',
|
|
44
|
+
]
|
|
45
|
+
const FG_OF = { b: BRIGHT, d: DEEP, c: CREAM, k: DARK }
|
|
46
|
+
const BG_OF = { b: BG_BRIGHT, d: BG_DEEP, c: BG_CREAM, k: BG_DARK }
|
|
47
|
+
const GRID_WIDTH = GRID[0].length
|
|
48
|
+
|
|
49
|
+
function renderButterflyLines() {
|
|
50
|
+
const out = []
|
|
51
|
+
for (let r = 0; r < GRID.length; r += 2) {
|
|
52
|
+
const top = GRID[r] || '.'.repeat(GRID_WIDTH)
|
|
53
|
+
const bot = GRID[r + 1] || '.'.repeat(GRID_WIDTH)
|
|
54
|
+
let line = ''
|
|
55
|
+
for (let col = 0; col < GRID_WIDTH; col++) {
|
|
56
|
+
const tCh = top[col]
|
|
57
|
+
const bCh = bot[col]
|
|
58
|
+
const tFg = FG_OF[tCh]
|
|
59
|
+
const bFg = FG_OF[bCh]
|
|
60
|
+
if (!tFg && !bFg) line += ' '
|
|
61
|
+
else if (tFg && !bFg) line += c(tFg) + '▀' + c(RESET)
|
|
62
|
+
else if (!tFg && bFg) line += c(bFg) + '▄' + c(RESET)
|
|
63
|
+
else if (tCh === bCh) line += c(tFg) + '█' + c(RESET)
|
|
64
|
+
else line += c(tFg) + c(BG_OF[bCh]) + '▀' + c(RESET)
|
|
65
|
+
}
|
|
66
|
+
out.push(line)
|
|
67
|
+
}
|
|
68
|
+
return out
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const WORDMARK_LINES = [
|
|
72
|
+
' _ _ ',
|
|
73
|
+
' _ __ ___ _ _ _ __ __ _ ___ __ _ | | _(_)',
|
|
74
|
+
"| '_ ` _ \\| | | | '__/ _` / __|/ _` || |/ /| |",
|
|
75
|
+
'| | | | | | |_| | | | (_| \\__ \\ (_| || < | |',
|
|
76
|
+
'|_| |_| |_|\\__,_|_| \\__,_|___/\\__,_||_|\\_\\|_|',
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
function colorize(line, color, opts = {}) {
|
|
80
|
+
return (opts.bold ? c(BOLD) : '') + c(color) + line + c(RESET)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function renderBanner() {
|
|
84
|
+
const bf = renderButterflyLines()
|
|
85
|
+
const wm = WORDMARK_LINES.map((l) => colorize(l, BRIGHT, { bold: true }))
|
|
86
|
+
const gap = ' '
|
|
87
|
+
const total = Math.max(bf.length, wm.length)
|
|
88
|
+
const wmOffset = Math.max(0, Math.floor((bf.length - wm.length) / 2))
|
|
89
|
+
const blankBf = ' '.repeat(GRID_WIDTH)
|
|
90
|
+
const lines = []
|
|
91
|
+
for (let i = 0; i < total; i++) {
|
|
92
|
+
const bfLine = bf[i] !== undefined ? bf[i] : blankBf
|
|
93
|
+
const wmIdx = i - wmOffset
|
|
94
|
+
const wmLine = (wmIdx >= 0 && wmIdx < wm.length) ? wm[wmIdx] : ''
|
|
95
|
+
lines.push(' ' + bfLine + gap + wmLine)
|
|
96
|
+
}
|
|
97
|
+
return lines.join('\n')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Output helpers ─────────────────────────────────────────────────────
|
|
101
|
+
const log = (s) => process.stdout.write(s + '\n')
|
|
102
|
+
|
|
103
|
+
// ── Validation ─────────────────────────────────────────────────────────
|
|
104
|
+
function isValidPackageName(name) {
|
|
105
|
+
// Basic npm package name rules
|
|
106
|
+
return /^[a-z0-9][a-z0-9._-]*$/.test(name)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function promptForName() {
|
|
110
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
111
|
+
const answer = await rl.question(` ${c(DEEP)}?${c(RESET)} ${c(BOLD)}Project name${c(RESET)} ${c(DIM)}(my-app):${c(RESET)} `)
|
|
112
|
+
rl.close()
|
|
113
|
+
return answer.trim() || 'my-app'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Scaffold ───────────────────────────────────────────────────────────
|
|
117
|
+
async function scaffold(projectName) {
|
|
118
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
119
|
+
const templateDir = join(__dirname, 'templates', 'default')
|
|
120
|
+
const targetDir = resolve(process.cwd(), projectName)
|
|
121
|
+
|
|
122
|
+
// Validate
|
|
123
|
+
if (!isValidPackageName(projectName)) {
|
|
124
|
+
log(`\n ${c(RED)}✗${c(RESET)} Invalid project name: ${c(BOLD)}${projectName}${c(RESET)}`)
|
|
125
|
+
log(` ${c(DIM)}must match: ${c(RESET)}${c(DIM)}/^[a-z0-9][a-z0-9._-]*$/${c(RESET)}\n`)
|
|
126
|
+
process.exit(1)
|
|
127
|
+
}
|
|
128
|
+
if (existsSync(targetDir)) {
|
|
129
|
+
log(`\n ${c(RED)}✗${c(RESET)} Target directory already exists: ${c(BOLD)}${targetDir}${c(RESET)}\n`)
|
|
130
|
+
process.exit(1)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Copy template
|
|
134
|
+
log(`\n ${c(DIM)}○${c(RESET)} Creating ${c(BOLD)}${projectName}/${c(RESET)} from template...`)
|
|
135
|
+
await mkdir(targetDir, { recursive: true })
|
|
136
|
+
await cp(templateDir, targetDir, { recursive: true })
|
|
137
|
+
|
|
138
|
+
// Patch package.json name
|
|
139
|
+
const pkgPath = join(targetDir, 'package.json')
|
|
140
|
+
const pkgRaw = await readFile(pkgPath, 'utf8')
|
|
141
|
+
const pkgPatched = pkgRaw.replace(/"__PROJECT_NAME__"/, JSON.stringify(projectName))
|
|
142
|
+
await writeFile(pkgPath, pkgPatched)
|
|
143
|
+
|
|
144
|
+
log(` ${c(GREEN)}${c(BOLD)}✓${c(RESET)} Created ${c(BOLD)}${projectName}/${c(RESET)}`)
|
|
145
|
+
|
|
146
|
+
// Next steps
|
|
147
|
+
log(`
|
|
148
|
+
${c(DIM)}Next:${c(RESET)}
|
|
149
|
+
${c(BRIGHT)}cd${c(RESET)} ${projectName}
|
|
150
|
+
${c(BRIGHT)}npm install${c(RESET)} ${c(DIM)}# or pnpm install${c(RESET)}
|
|
151
|
+
${c(BRIGHT)}npm run dev${c(RESET)} ${c(DIM)}# starts the desktop window with HMR${c(RESET)}
|
|
152
|
+
|
|
153
|
+
${c(DIM)}docs${c(RESET)} ${c(DIM)}https://github.com/murasakijs/murasaki${c(RESET)}
|
|
10
154
|
`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Main ──────────────────────────────────────────────────────────────
|
|
158
|
+
const banner = renderBanner()
|
|
159
|
+
process.stdout.write('\n' + banner + '\n\n')
|
|
160
|
+
process.stdout.write(` ${c(DIM)}desktop apps for Next.js developers${c(RESET)}\n`)
|
|
161
|
+
|
|
162
|
+
const argName = process.argv[2]
|
|
163
|
+
const projectName = argName || (await promptForName())
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
await scaffold(projectName)
|
|
167
|
+
} catch (err) {
|
|
168
|
+
log(`\n ${c(RED)}✗${c(RESET)} Scaffold failed: ${err.message}\n`)
|
|
169
|
+
process.exit(1)
|
|
170
|
+
}
|
|
171
|
+
|
|
11
172
|
process.exit(0)
|
package/package.json
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "murasaki dev"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"murasaki": "^0.0.1",
|
|
11
|
+
"react": "^19.2.7",
|
|
12
|
+
"react-dom": "^19.2.7"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/react": "^19.2.17",
|
|
16
|
+
"@types/react-dom": "^19.2.3"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/app.tsx — your app. Edit me and the window reloads in place.
|
|
2
|
+
|
|
3
|
+
export default function App() {
|
|
4
|
+
return (
|
|
5
|
+
<main>
|
|
6
|
+
<h1>Hello, Murasaki 🦋</h1>
|
|
7
|
+
<p>This view lives in <code>src/app.tsx</code>.</p>
|
|
8
|
+
<p className="hint">Edit the file and the window reloads instantly.</p>
|
|
9
|
+
</main>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/layout.tsx — wraps your app. Edit me to change the global shell.
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
export default function Layout({ children }: { children: ReactNode }) {
|
|
6
|
+
return (
|
|
7
|
+
<html lang="en">
|
|
8
|
+
<head>
|
|
9
|
+
<meta charSet="utf-8" />
|
|
10
|
+
<title>Murasaki App</title>
|
|
11
|
+
<style>{`
|
|
12
|
+
:root { color-scheme: light dark; }
|
|
13
|
+
* { box-sizing: border-box; }
|
|
14
|
+
body {
|
|
15
|
+
margin: 0;
|
|
16
|
+
min-height: 100vh;
|
|
17
|
+
display: grid;
|
|
18
|
+
place-items: center;
|
|
19
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
20
|
+
background: linear-gradient(135deg, #faf8ff 0%, #f3eafe 100%);
|
|
21
|
+
color: #1a0a33;
|
|
22
|
+
}
|
|
23
|
+
@media (prefers-color-scheme: dark) {
|
|
24
|
+
body {
|
|
25
|
+
background: linear-gradient(135deg, #0a0612 0%, #1a0a33 100%);
|
|
26
|
+
color: #faf8ff;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
main { text-align: center; padding: 40px; }
|
|
30
|
+
h1 {
|
|
31
|
+
font-size: 96px;
|
|
32
|
+
margin: 0;
|
|
33
|
+
background: linear-gradient(135deg, #5B21B6 0%, #A855F7 100%);
|
|
34
|
+
-webkit-background-clip: text;
|
|
35
|
+
background-clip: text;
|
|
36
|
+
color: transparent;
|
|
37
|
+
font-weight: 800;
|
|
38
|
+
letter-spacing: -0.04em;
|
|
39
|
+
}
|
|
40
|
+
p { margin-top: 16px; font-size: 18px; opacity: 0.7; }
|
|
41
|
+
.hint { opacity: 0.45; font-size: 14px; }
|
|
42
|
+
code {
|
|
43
|
+
font-family: 'SF Mono', Menlo, monospace;
|
|
44
|
+
background: rgba(168, 85, 247, 0.12);
|
|
45
|
+
padding: 2px 8px;
|
|
46
|
+
border-radius: 4px;
|
|
47
|
+
font-size: 0.9em;
|
|
48
|
+
}
|
|
49
|
+
`}</style>
|
|
50
|
+
</head>
|
|
51
|
+
<body>{children}</body>
|
|
52
|
+
</html>
|
|
53
|
+
)
|
|
54
|
+
}
|