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 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) — ichi's revised Figma version ─
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 (!tFg && !bFg) {
57
- line += ' '
58
- } else if (tFg && !bFg) {
59
- line += c(tFg) + '▀' + c(RESET)
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
- const prefix = (opts.bold ? c(BOLD) : '') + c(color)
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() // now 6 lines
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('\n' + renderBanner() + '\n')
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
- ${c(DEEP)}docs${c(RESET)} ${c(DIM)}https://github.com/murasakijs/murasaki${c(RESET)}
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
- ${c(DIM)}🌱 Pre-alpha scaffolder not implemented yet.${c(RESET)}
118
- ${c(DIM)}Follow progress on GitHub or watch this space.${c(RESET)}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-murasaki",
3
- "version": "0.0.1",
3
+ "version": "0.0.4",
4
4
  "description": "Scaffolder for Murasaki apps. Run with `npm create murasaki@latest`.",
5
5
  "keywords": [
6
6
  "murasaki",
@@ -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
+ }