create-murasaki 0.0.1 → 0.0.4
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 +126 -34
- 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,5 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// create-murasaki — Scaffolder for Murasaki apps.
|
|
3
|
+
// Usage: npm create murasaki@latest my-app
|
|
4
|
+
|
|
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'
|
|
10
|
+
import { spawnSync } from 'node:child_process'
|
|
3
11
|
|
|
4
12
|
// ── ANSI truecolor (Oomurasaki palette) ────────────────────────────────
|
|
5
13
|
const BRIGHT = '\x1b[38;2;168;85;247m'
|
|
@@ -7,10 +15,11 @@ const DEEP = '\x1b[38;2;91;33;182m'
|
|
|
7
15
|
const CREAM = '\x1b[38;2;250;245;232m'
|
|
8
16
|
const DARK = '\x1b[38;2;59;7;100m'
|
|
9
17
|
const DIM = '\x1b[38;2;136;136;153m'
|
|
18
|
+
const GREEN = '\x1b[38;2;76;175;80m'
|
|
19
|
+
const RED = '\x1b[38;2;239;68;68m'
|
|
10
20
|
const BOLD = '\x1b[1m'
|
|
11
21
|
const RESET = '\x1b[0m'
|
|
12
22
|
|
|
13
|
-
// Background versions for half-block compositing
|
|
14
23
|
const BG_BRIGHT = '\x1b[48;2;168;85;247m'
|
|
15
24
|
const BG_DEEP = '\x1b[48;2;91;33;182m'
|
|
16
25
|
const BG_CREAM = '\x1b[48;2;250;245;232m'
|
|
@@ -19,7 +28,7 @@ const BG_DARK = '\x1b[48;2;59;7;100m'
|
|
|
19
28
|
const noColor = process.env.NO_COLOR || !process.stdout.isTTY
|
|
20
29
|
const c = (code) => (noColor ? '' : code)
|
|
21
30
|
|
|
22
|
-
// ── H4 butterfly grid (19 col × 12 row)
|
|
31
|
+
// ── H4 butterfly grid (19 col × 12 row) ────────────────────────────────
|
|
23
32
|
const GRID = [
|
|
24
33
|
'.....b.......b.....',
|
|
25
34
|
'......b.....b......',
|
|
@@ -34,13 +43,10 @@ const GRID = [
|
|
|
34
43
|
'....ddddd.ddddd....',
|
|
35
44
|
'.....dddd.dddd.....',
|
|
36
45
|
]
|
|
37
|
-
|
|
38
46
|
const FG_OF = { b: BRIGHT, d: DEEP, c: CREAM, k: DARK }
|
|
39
47
|
const BG_OF = { b: BG_BRIGHT, d: BG_DEEP, c: BG_CREAM, k: BG_DARK }
|
|
40
48
|
const GRID_WIDTH = GRID[0].length
|
|
41
49
|
|
|
42
|
-
// Compress 2 grid rows into 1 terminal row using half-block ▀
|
|
43
|
-
// (top half = fg color, bottom half = bg color)
|
|
44
50
|
function renderButterflyLines() {
|
|
45
51
|
const out = []
|
|
46
52
|
for (let r = 0; r < GRID.length; r += 2) {
|
|
@@ -52,28 +58,17 @@ function renderButterflyLines() {
|
|
|
52
58
|
const bCh = bot[col]
|
|
53
59
|
const tFg = FG_OF[tCh]
|
|
54
60
|
const bFg = FG_OF[bCh]
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
} else if (!tFg && bFg) {
|
|
61
|
-
line += c(bFg) + '▄' + c(RESET)
|
|
62
|
-
} else {
|
|
63
|
-
// Both halves filled
|
|
64
|
-
if (tCh === bCh) {
|
|
65
|
-
line += c(tFg) + '█' + c(RESET)
|
|
66
|
-
} else {
|
|
67
|
-
line += c(tFg) + c(BG_OF[bCh]) + '▀' + c(RESET)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
61
|
+
if (!tFg && !bFg) line += ' '
|
|
62
|
+
else if (tFg && !bFg) line += c(tFg) + '▀' + c(RESET)
|
|
63
|
+
else if (!tFg && bFg) line += c(bFg) + '▄' + c(RESET)
|
|
64
|
+
else if (tCh === bCh) line += c(tFg) + '█' + c(RESET)
|
|
65
|
+
else line += c(tFg) + c(BG_OF[bCh]) + '▀' + c(RESET)
|
|
70
66
|
}
|
|
71
67
|
out.push(line)
|
|
72
68
|
}
|
|
73
69
|
return out
|
|
74
70
|
}
|
|
75
71
|
|
|
76
|
-
// ── figlet "Standard" wordmark ─────────────────────────────────────────
|
|
77
72
|
const WORDMARK_LINES = [
|
|
78
73
|
' _ _ ',
|
|
79
74
|
' _ __ ___ _ _ _ __ __ _ ___ __ _ | | _(_)',
|
|
@@ -83,20 +78,16 @@ const WORDMARK_LINES = [
|
|
|
83
78
|
]
|
|
84
79
|
|
|
85
80
|
function colorize(line, color, opts = {}) {
|
|
86
|
-
|
|
87
|
-
return prefix + line + c(RESET)
|
|
81
|
+
return (opts.bold ? c(BOLD) : '') + c(color) + line + c(RESET)
|
|
88
82
|
}
|
|
89
83
|
|
|
90
|
-
// ── Render banner: butterfly LEFT, wordmark RIGHT, vertically centered ─
|
|
91
84
|
function renderBanner() {
|
|
92
|
-
const bf = renderButterflyLines()
|
|
85
|
+
const bf = renderButterflyLines()
|
|
93
86
|
const wm = WORDMARK_LINES.map((l) => colorize(l, BRIGHT, { bold: true }))
|
|
94
87
|
const gap = ' '
|
|
95
|
-
|
|
96
88
|
const total = Math.max(bf.length, wm.length)
|
|
97
89
|
const wmOffset = Math.max(0, Math.floor((bf.length - wm.length) / 2))
|
|
98
90
|
const blankBf = ' '.repeat(GRID_WIDTH)
|
|
99
|
-
|
|
100
91
|
const lines = []
|
|
101
92
|
for (let i = 0; i < total; i++) {
|
|
102
93
|
const bfLine = bf[i] !== undefined ? bf[i] : blankBf
|
|
@@ -107,16 +98,117 @@ function renderBanner() {
|
|
|
107
98
|
return lines.join('\n')
|
|
108
99
|
}
|
|
109
100
|
|
|
110
|
-
// ── Output
|
|
111
|
-
process.stdout.write(
|
|
112
|
-
process.stdout.write(`
|
|
113
|
-
${c(DIM)}desktop apps for Next.js developers${c(RESET)}
|
|
101
|
+
// ── Output helpers ─────────────────────────────────────────────────────
|
|
102
|
+
const log = (s) => process.stdout.write(s + '\n')
|
|
114
103
|
|
|
115
|
-
|
|
104
|
+
// ── Validation ─────────────────────────────────────────────────────────
|
|
105
|
+
function isValidPackageName(name) {
|
|
106
|
+
// Basic npm package name rules
|
|
107
|
+
return /^[a-z0-9][a-z0-9._-]*$/.test(name)
|
|
108
|
+
}
|
|
116
109
|
|
|
117
|
-
|
|
118
|
-
|
|
110
|
+
// ── Package manager detection ─────────────────────────────────────────
|
|
111
|
+
function detectPackageManager() {
|
|
112
|
+
const ua = process.env.npm_config_user_agent || ''
|
|
113
|
+
if (ua.startsWith('pnpm')) return 'pnpm'
|
|
114
|
+
if (ua.startsWith('yarn')) return 'yarn'
|
|
115
|
+
if (ua.startsWith('bun')) return 'bun'
|
|
116
|
+
return 'npm'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function installArgs(pm) {
|
|
120
|
+
// pnpm respects parent workspace by default; --ignore-workspace makes the
|
|
121
|
+
// new app's install self-contained even when scaffolded inside a workspace.
|
|
122
|
+
if (pm === 'pnpm') return ['install', '--ignore-workspace']
|
|
123
|
+
return ['install']
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function runInstall(targetDir, pm) {
|
|
127
|
+
const result = spawnSync(pm, installArgs(pm), {
|
|
128
|
+
cwd: targetDir,
|
|
129
|
+
stdio: 'inherit',
|
|
130
|
+
shell: process.platform === 'win32',
|
|
131
|
+
})
|
|
132
|
+
return result.status === 0
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function promptForName() {
|
|
136
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
137
|
+
const answer = await rl.question(` ${c(DEEP)}?${c(RESET)} ${c(BOLD)}Project name${c(RESET)} ${c(DIM)}(my-app):${c(RESET)} `)
|
|
138
|
+
rl.close()
|
|
139
|
+
return answer.trim() || 'my-app'
|
|
140
|
+
}
|
|
119
141
|
|
|
142
|
+
// ── Scaffold ───────────────────────────────────────────────────────────
|
|
143
|
+
async function scaffold(projectName) {
|
|
144
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
145
|
+
const templateDir = join(__dirname, 'templates', 'default')
|
|
146
|
+
const targetDir = resolve(process.cwd(), projectName)
|
|
147
|
+
|
|
148
|
+
// Validate
|
|
149
|
+
if (!isValidPackageName(projectName)) {
|
|
150
|
+
log(`\n ${c(RED)}✗${c(RESET)} Invalid project name: ${c(BOLD)}${projectName}${c(RESET)}`)
|
|
151
|
+
log(` ${c(DIM)}must match: ${c(RESET)}${c(DIM)}/^[a-z0-9][a-z0-9._-]*$/${c(RESET)}\n`)
|
|
152
|
+
process.exit(1)
|
|
153
|
+
}
|
|
154
|
+
if (existsSync(targetDir)) {
|
|
155
|
+
log(`\n ${c(RED)}✗${c(RESET)} Target directory already exists: ${c(BOLD)}${targetDir}${c(RESET)}\n`)
|
|
156
|
+
process.exit(1)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Copy template
|
|
160
|
+
log(`\n ${c(DIM)}○${c(RESET)} Creating ${c(BOLD)}${projectName}/${c(RESET)} from template...`)
|
|
161
|
+
await mkdir(targetDir, { recursive: true })
|
|
162
|
+
await cp(templateDir, targetDir, { recursive: true })
|
|
163
|
+
|
|
164
|
+
// Patch package.json name
|
|
165
|
+
const pkgPath = join(targetDir, 'package.json')
|
|
166
|
+
const pkgRaw = await readFile(pkgPath, 'utf8')
|
|
167
|
+
const pkgPatched = pkgRaw.replace(/"__PROJECT_NAME__"/, JSON.stringify(projectName))
|
|
168
|
+
await writeFile(pkgPath, pkgPatched)
|
|
169
|
+
|
|
170
|
+
log(` ${c(GREEN)}${c(BOLD)}✓${c(RESET)} Created ${c(BOLD)}${projectName}/${c(RESET)}`)
|
|
171
|
+
|
|
172
|
+
// Install dependencies (Next.js-like behavior)
|
|
173
|
+
const pm = detectPackageManager()
|
|
174
|
+
const skip = process.argv.includes('--skip-install')
|
|
175
|
+
|
|
176
|
+
if (!skip) {
|
|
177
|
+
log(` ${c(DIM)}○${c(RESET)} Installing dependencies with ${c(BOLD)}${pm}${c(RESET)}...\n`)
|
|
178
|
+
const installStart = Date.now()
|
|
179
|
+
const ok = runInstall(targetDir, pm)
|
|
180
|
+
const elapsed = ((Date.now() - installStart) / 1000).toFixed(1)
|
|
181
|
+
if (ok) {
|
|
182
|
+
log(`\n ${c(GREEN)}${c(BOLD)}✓${c(RESET)} Dependencies installed ${c(DIM)}(${elapsed}s)${c(RESET)}`)
|
|
183
|
+
} else {
|
|
184
|
+
log(`\n ${c(RED)}✗${c(RESET)} ${pm} install failed; run ${c(BOLD)}${pm} install${c(RESET)} manually inside ${c(BOLD)}${projectName}/${c(RESET)}`)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Next steps
|
|
189
|
+
const runner = pm === 'npm' ? 'npm run dev' : `${pm} dev`
|
|
190
|
+
log(`
|
|
191
|
+
${c(DIM)}Next:${c(RESET)}
|
|
192
|
+
${c(BRIGHT)}cd${c(RESET)} ${projectName}
|
|
193
|
+
${c(BRIGHT)}${runner}${c(RESET)} ${c(DIM)}# starts the desktop window with HMR${c(RESET)}
|
|
194
|
+
|
|
195
|
+
${c(DIM)}docs${c(RESET)} ${c(DIM)}https://github.com/murasakijs/murasaki${c(RESET)}
|
|
120
196
|
`)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Main ──────────────────────────────────────────────────────────────
|
|
200
|
+
const banner = renderBanner()
|
|
201
|
+
process.stdout.write('\n' + banner + '\n\n')
|
|
202
|
+
process.stdout.write(` ${c(DIM)}desktop apps for Next.js developers${c(RESET)}\n`)
|
|
203
|
+
|
|
204
|
+
const argName = process.argv[2]
|
|
205
|
+
const projectName = argName || (await promptForName())
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await scaffold(projectName)
|
|
209
|
+
} catch (err) {
|
|
210
|
+
log(`\n ${c(RED)}✗${c(RESET)} Scaffold failed: ${err.message}\n`)
|
|
211
|
+
process.exit(1)
|
|
212
|
+
}
|
|
121
213
|
|
|
122
214
|
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.2",
|
|
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
|
+
}
|