@zenithbuild/cli 0.4.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.
@@ -0,0 +1,467 @@
1
+ /**
2
+ * @zenithbuild/cli - Dev Command
3
+ *
4
+ * Development server with HMR support.
5
+ *
6
+ * ═══════════════════════════════════════════════════════════════════════════════
7
+ * CLI HARDENING: BLIND ORCHESTRATOR PATTERN
8
+ * ═══════════════════════════════════════════════════════════════════════════════
9
+ *
10
+ * This file follows the CLI Hardening Plan:
11
+ * - NO plugin-specific branching (no `if (hasContentPlugin)`)
12
+ * - NO semantic helpers (no `getContentData()`)
13
+ * - NO plugin type imports or casts
14
+ * - ONLY opaque data forwarding via hooks
15
+ *
16
+ * The CLI dispatches lifecycle hooks and collects payloads.
17
+ * It never understands what the data means.
18
+ * ═══════════════════════════════════════════════════════════════════════════════
19
+ */
20
+
21
+ import path from 'path'
22
+ import fs from 'fs'
23
+ import { serve, type ServerWebSocket } from 'bun'
24
+ import { requireProject } from '../utils/project'
25
+ import * as logger from '../utils/logger'
26
+ import * as brand from '../utils/branding'
27
+ import {
28
+ compileZenSource,
29
+ discoverLayouts,
30
+ processLayout,
31
+ generateBundleJS,
32
+ loadZenithConfig,
33
+ PluginRegistry,
34
+ createPluginContext,
35
+ getPluginDataByNamespace,
36
+ compileCssAsync,
37
+ resolveGlobalsCss,
38
+ createBridgeAPI,
39
+ runPluginHooks,
40
+ collectHookReturns,
41
+ buildRuntimeEnvelope,
42
+ clearHooks,
43
+ type HookContext
44
+ } from '@zenithbuild/compiler'
45
+
46
+ export interface DevOptions {
47
+ port?: number
48
+ }
49
+
50
+ interface CompiledPage {
51
+ html: string
52
+ script: string
53
+ styles: string[]
54
+ route: string
55
+ lastModified: number
56
+ }
57
+
58
+ const pageCache = new Map<string, CompiledPage>()
59
+
60
+ /**
61
+ * Bundle page script using Rolldown to resolve npm imports at compile time.
62
+ * Only called when compiler emits a BundlePlan - bundler performs no inference.
63
+ */
64
+ import { bundlePageScript, type BundlePlan, generateRouteDefinition } from '@zenithbuild/compiler'
65
+
66
+ export async function dev(options: DevOptions = {}): Promise<void> {
67
+ const project = requireProject()
68
+ const port = options.port || parseInt(process.env.PORT || '3000', 10)
69
+ const pagesDir = project.pagesDir
70
+ const rootDir = project.root
71
+
72
+ // Load zenith.config.ts if present
73
+ const config = await loadZenithConfig(rootDir)
74
+ const registry = new PluginRegistry()
75
+ const bridgeAPI = createBridgeAPI()
76
+
77
+ // Clear any previously registered hooks (important for restarts)
78
+ clearHooks()
79
+
80
+ console.log('[Zenith] Config plugins:', config.plugins?.length ?? 0)
81
+
82
+ // ============================================
83
+ // Plugin Registration (Unconditional)
84
+ // ============================================
85
+ // CLI registers ALL plugins without checking which ones exist.
86
+ // Each plugin decides what hooks to register.
87
+ for (const plugin of config.plugins || []) {
88
+ console.log('[Zenith] Registering plugin:', plugin.name)
89
+ registry.register(plugin)
90
+
91
+ // Let plugin register its CLI hooks (if it wants to)
92
+ // CLI does NOT check what the plugin is - it just offers the API
93
+ if (plugin.registerCLI) {
94
+ plugin.registerCLI(bridgeAPI)
95
+ }
96
+ }
97
+
98
+ // ============================================
99
+ // Plugin Initialization (Unconditional)
100
+ // ============================================
101
+ // Initialize ALL plugins unconditionally.
102
+ // If no plugins, this is a no-op. CLI doesn't branch on plugin presence.
103
+ await registry.initAll(createPluginContext(rootDir))
104
+
105
+ // Create hook context - CLI provides this but NEVER uses getPluginData itself
106
+ const hookCtx: HookContext = {
107
+ projectRoot: rootDir,
108
+ getPluginData: getPluginDataByNamespace
109
+ }
110
+
111
+ // Dispatch lifecycle hook - plugins decide if they care
112
+ await runPluginHooks('cli:dev:start', hookCtx)
113
+
114
+ // ============================================
115
+ // CSS Compilation (Compiler-Owned)
116
+ // ============================================
117
+ const globalsCssPath = resolveGlobalsCss(rootDir)
118
+ let compiledCss = ''
119
+
120
+ if (globalsCssPath) {
121
+ console.log('[Zenith] Compiling CSS:', path.relative(rootDir, globalsCssPath))
122
+ const cssResult = await compileCssAsync({ input: globalsCssPath, output: ':memory:' })
123
+ if (cssResult.success) {
124
+ compiledCss = cssResult.css
125
+ console.log(`[Zenith] CSS compiled in ${cssResult.duration}ms`)
126
+ } else {
127
+ console.error('[Zenith] CSS compilation failed:', cssResult.error)
128
+ }
129
+ }
130
+
131
+ const clients = new Set<ServerWebSocket<unknown>>()
132
+
133
+ // Branded Startup Panel
134
+ brand.showServerPanel({
135
+ project: project.root,
136
+ pages: project.pagesDir,
137
+ url: `http://localhost:${port}`,
138
+ hmr: true,
139
+ mode: 'In-memory compilation'
140
+ })
141
+
142
+ // File extensions that should be served as static assets
143
+ const STATIC_EXTENSIONS = new Set([
144
+ '.js', '.css', '.ico', '.png', '.jpg', '.jpeg', '.gif', '.svg',
145
+ '.webp', '.woff', '.woff2', '.ttf', '.eot', '.json', '.map'
146
+ ])
147
+
148
+ /**
149
+ * Compile a .zen page in memory
150
+ */
151
+ async function compilePageInMemory(pagePath: string): Promise<CompiledPage | null> {
152
+ try {
153
+ const layoutsDir = path.join(pagesDir, '../layouts')
154
+ const componentsDir = path.join(pagesDir, '../components')
155
+ const layouts = discoverLayouts(layoutsDir)
156
+ const source = fs.readFileSync(pagePath, 'utf-8')
157
+
158
+ let processedSource = source
159
+ let layoutToUse = layouts.get('DefaultLayout')
160
+
161
+ if (layoutToUse) processedSource = processLayout(source, layoutToUse)
162
+
163
+ const result = await compileZenSource(processedSource, pagePath, {
164
+ componentsDir: fs.existsSync(componentsDir) ? componentsDir : undefined
165
+ })
166
+ if (!result.finalized) throw new Error('Compilation failed')
167
+
168
+ const routeDef = generateRouteDefinition(pagePath, pagesDir)
169
+
170
+
171
+
172
+ // Safely strip imports from the top of the script
173
+ // This relies on the fact that duplicate imports (from Rust codegen)
174
+ // appear at the beginning of result.finalized.js
175
+ let jsLines = result.finalized.js.split('\n')
176
+
177
+ // Remove lines from top that are imports, whitespace, or comments
178
+ while (jsLines.length > 0 && jsLines[0] !== undefined) {
179
+ const line = jsLines[0].trim()
180
+ if (
181
+ line.startsWith('import ') ||
182
+ line === '' ||
183
+ line.startsWith('//') ||
184
+ line.startsWith('/*') ||
185
+ line.startsWith('*')
186
+ ) {
187
+ jsLines.shift()
188
+ } else {
189
+ break
190
+ }
191
+ }
192
+
193
+ let jsWithoutImports = jsLines.join('\n')
194
+
195
+ // PATCH: Fix unquoted keys with dashes (Rust codegen bug in jsx_lowerer)
196
+ // e.g. stroke-width: "1.5" -> "stroke-width": "1.5"
197
+ // We only apply this to the JS portions (Script and Expressions)
198
+ // to avoid corrupting the Styles section.
199
+ const stylesMarker = '// 6. Styles injection'
200
+ const parts = jsWithoutImports.split(stylesMarker)
201
+
202
+ if (parts.length > 1) {
203
+ // Apply patch only to the JS part
204
+ parts[0] = parts[0]!.replace(
205
+ /(^|[{,])\s*([a-zA-Z][a-zA-Z0-9-]*-[a-zA-Z0-9-]*)\s*:/gm,
206
+ '$1"$2":'
207
+ )
208
+ jsWithoutImports = parts.join(stylesMarker)
209
+ } else {
210
+ // Fallback if marker not found
211
+ jsWithoutImports = jsWithoutImports.replace(
212
+ /(^|[{,])\s*([a-zA-Z][a-zA-Z0-9-]*-[a-zA-Z0-9-]*)\s*:/gm,
213
+ '$1"$2":'
214
+ )
215
+ }
216
+
217
+ // Combine: structured imports first, then cleaned script body
218
+ const fullScript = (result.finalized.npmImports || '') + '\n\n' + jsWithoutImports
219
+
220
+ console.log('[Dev] Page Imports:', result.finalized.npmImports ? result.finalized.npmImports.split('\n').length : 0, 'lines')
221
+
222
+ // Bundle ONLY if compiler emitted a BundlePlan (no inference)
223
+ let bundledScript = fullScript
224
+ if (result.finalized.bundlePlan) {
225
+ // Compiler decided bundling is needed - pass plan with proper resolve roots
226
+ const plan: BundlePlan = {
227
+ ...result.finalized.bundlePlan,
228
+ entry: fullScript,
229
+ resolveRoots: [path.join(rootDir, 'node_modules'), 'node_modules']
230
+ }
231
+ bundledScript = await bundlePageScript(plan)
232
+ }
233
+
234
+ return {
235
+ html: result.finalized.html,
236
+ script: bundledScript,
237
+ styles: result.finalized.styles,
238
+ route: routeDef.path,
239
+ lastModified: Date.now()
240
+ }
241
+ } catch (error: any) {
242
+ logger.error(`Compilation error: ${error.message}`)
243
+ return null
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Generate dev HTML with plugin data envelope
249
+ *
250
+ * CLI collects payloads from plugins via 'cli:runtime:collect' hook.
251
+ * It serializes blindly - never inspecting what's inside.
252
+ */
253
+ async function generateDevHTML(page: CompiledPage): Promise<string> {
254
+ // Single neutral injection point
255
+ const runtimeTag = `<script src="/runtime.js"></script>`
256
+ const scriptTag = `<script type="module">\n${page.script}\n</script>`
257
+ const allScripts = `${runtimeTag}\n${scriptTag}`
258
+
259
+ let html = page.html.includes('</body>')
260
+ ? page.html.replace('</body>', `${allScripts}\n</body>`)
261
+ : `${page.html}\n${allScripts}`
262
+
263
+ // Ensure DOCTYPE is present to prevent Quirks Mode (critical for SVG namespace)
264
+ if (!html.trimStart().toLowerCase().startsWith('<!doctype')) {
265
+ html = `<!DOCTYPE html>\n${html}`
266
+ }
267
+
268
+ return html
269
+ }
270
+
271
+ // ============================================
272
+ // File Watcher (Plugin-Agnostic)
273
+ // ============================================
274
+ // CLI watches files but delegates decisions to plugins via hooks.
275
+ // No branching on file types that are "content" vs "not content".
276
+ const watcher = fs.watch(path.join(pagesDir, '..'), { recursive: true }, async (event, filename) => {
277
+ if (!filename) return
278
+
279
+ // Dispatch file change hook to ALL plugins
280
+ // Each plugin decides if it cares about this file
281
+ await runPluginHooks('cli:dev:file-change', {
282
+ ...hookCtx,
283
+ filename,
284
+ event
285
+ })
286
+
287
+ if (filename.endsWith('.zen')) {
288
+ logger.hmr('Page', filename)
289
+
290
+ // Clear page cache to force fresh compilation on next request
291
+ pageCache.clear()
292
+
293
+ // Recompile CSS for new Tailwind classes in .zen files
294
+ if (globalsCssPath) {
295
+ const cssResult = await compileCssAsync({ input: globalsCssPath, output: ':memory:' })
296
+ if (cssResult.success) {
297
+ compiledCss = cssResult.css
298
+ }
299
+ }
300
+
301
+ // Broadcast page reload AFTER cache cleared and CSS ready
302
+ for (const client of clients) {
303
+ client.send(JSON.stringify({ type: 'reload' }))
304
+ }
305
+ } else if (filename.endsWith('.css')) {
306
+ logger.hmr('CSS', filename)
307
+ // Recompile CSS
308
+ if (globalsCssPath) {
309
+ const cssResult = await compileCssAsync({ input: globalsCssPath, output: ':memory:' })
310
+ if (cssResult.success) {
311
+ compiledCss = cssResult.css
312
+ }
313
+ }
314
+ for (const client of clients) {
315
+ client.send(JSON.stringify({ type: 'style-update', url: '/assets/styles.css' }))
316
+ }
317
+ } else {
318
+ // For all other file changes, re-initialize plugins unconditionally
319
+ // Plugins decide internally whether they need to reload data
320
+ // CLI does NOT branch on "is this a content file"
321
+ await registry.initAll(createPluginContext(rootDir))
322
+
323
+ // Broadcast reload for any non-code file changes
324
+ for (const client of clients) {
325
+ client.send(JSON.stringify({ type: 'reload' }))
326
+ }
327
+ }
328
+ })
329
+
330
+ const server = serve({
331
+ port,
332
+ async fetch(req, server) {
333
+ const startTime = performance.now()
334
+ const url = new URL(req.url)
335
+ const pathname = url.pathname
336
+ const ext = path.extname(pathname).toLowerCase()
337
+
338
+ // Upgrade to WebSocket for HMR
339
+ if (pathname === '/hmr') {
340
+ const upgraded = server.upgrade(req)
341
+ if (upgraded) return undefined
342
+ }
343
+
344
+ // Handle Zenith assets
345
+ if (pathname === '/runtime.js') {
346
+ // Collect runtime payloads from ALL plugins
347
+ const payloads = await collectHookReturns('cli:runtime:collect', hookCtx)
348
+ const envelope = buildRuntimeEnvelope(payloads)
349
+
350
+ const response = new Response(generateBundleJS(envelope), {
351
+ headers: { 'Content-Type': 'application/javascript; charset=utf-8' }
352
+ })
353
+ logger.route('GET', pathname, 200, Math.round(performance.now() - startTime), 0, Math.round(performance.now() - startTime))
354
+ return response
355
+ }
356
+
357
+ // Serve compiler-owned CSS (Tailwind compiled)
358
+ if (pathname === '/assets/styles.css') {
359
+ const response = new Response(compiledCss, {
360
+ headers: { 'Content-Type': 'text/css; charset=utf-8' }
361
+ })
362
+ logger.route('GET', pathname, 200, Math.round(performance.now() - startTime), 0, Math.round(performance.now() - startTime))
363
+ return response
364
+ }
365
+
366
+ // Legacy: also support /styles/globals.css or /styles/global.css for backwards compat
367
+ if (pathname === '/styles/globals.css' || pathname === '/styles/global.css') {
368
+ const response = new Response(compiledCss, {
369
+ headers: { 'Content-Type': 'text/css; charset=utf-8' }
370
+ })
371
+ logger.route('GET', pathname, 200, Math.round(performance.now() - startTime), 0, Math.round(performance.now() - startTime))
372
+ return response
373
+ }
374
+
375
+ // Static files
376
+ if (STATIC_EXTENSIONS.has(ext)) {
377
+ const publicPath = path.join(pagesDir, '../public', pathname)
378
+ if (fs.existsSync(publicPath)) {
379
+ const response = new Response(Bun.file(publicPath))
380
+ logger.route('GET', pathname, 200, Math.round(performance.now() - startTime), 0, Math.round(performance.now() - startTime))
381
+ return response
382
+ }
383
+ }
384
+
385
+ // Zenith Pages
386
+ const pagePath = findPageForRoute(pathname, pagesDir)
387
+ if (pagePath) {
388
+ const compileStart = performance.now()
389
+ let cached = pageCache.get(pagePath)
390
+ const stat = fs.statSync(pagePath)
391
+
392
+ if (!cached || stat.mtimeMs > cached.lastModified) {
393
+ cached = await compilePageInMemory(pagePath) || undefined
394
+ if (cached) pageCache.set(pagePath, cached)
395
+ }
396
+ const compileEnd = performance.now()
397
+
398
+ if (cached) {
399
+ const renderStart = performance.now()
400
+ const html = await generateDevHTML(cached)
401
+ const renderEnd = performance.now()
402
+
403
+ const totalTime = Math.round(performance.now() - startTime)
404
+ const compileTime = Math.round(compileEnd - compileStart)
405
+ const renderTime = Math.round(renderEnd - renderStart)
406
+
407
+ logger.route('GET', pathname, 200, totalTime, compileTime, renderTime)
408
+ return new Response(html, { headers: { 'Content-Type': 'text/html' } })
409
+ }
410
+ }
411
+
412
+ logger.route('GET', pathname, 404, Math.round(performance.now() - startTime), 0, 0)
413
+ return new Response('Not Found', { status: 404 })
414
+ },
415
+ websocket: {
416
+ open(ws) {
417
+ clients.add(ws)
418
+ },
419
+ close(ws) {
420
+ clients.delete(ws)
421
+ },
422
+ message() { }
423
+ }
424
+ })
425
+
426
+ process.on('SIGINT', () => {
427
+ watcher.close()
428
+ server.stop()
429
+ process.exit(0)
430
+ })
431
+
432
+ await new Promise(() => { })
433
+ }
434
+
435
+ function findPageForRoute(route: string, pagesDir: string): string | null {
436
+ // 1. Try exact match first (e.g., /about -> about.zen)
437
+ const exactPath = path.join(pagesDir, route === '/' ? 'index.zen' : `${route.slice(1)}.zen`)
438
+ if (fs.existsSync(exactPath)) return exactPath
439
+
440
+ // 2. Try index.zen in directory (e.g., /about -> about/index.zen)
441
+ const indexPath = path.join(pagesDir, route === '/' ? 'index.zen' : `${route.slice(1)}/index.zen`)
442
+ if (fs.existsSync(indexPath)) return indexPath
443
+
444
+ // 3. Try dynamic routes [slug].zen, [...slug].zen
445
+ // Walk up the path looking for dynamic segments
446
+ const segments = route === '/' ? [] : route.slice(1).split('/').filter(Boolean)
447
+
448
+ // Try matching with dynamic [slug].zen at each level
449
+ for (let i = segments.length - 1; i >= 0; i--) {
450
+ const staticPart = segments.slice(0, i).join('/')
451
+ const baseDir = staticPart ? path.join(pagesDir, staticPart) : pagesDir
452
+
453
+ // Check for [slug].zen (single segment catch)
454
+ const singleDynamicPath = path.join(baseDir, '[slug].zen')
455
+ if (fs.existsSync(singleDynamicPath)) return singleDynamicPath
456
+
457
+ // Check for [...slug].zen (catch-all)
458
+ const catchAllPath = path.join(baseDir, '[...slug].zen')
459
+ if (fs.existsSync(catchAllPath)) return catchAllPath
460
+ }
461
+
462
+ // 4. Check for catch-all at root
463
+ const rootCatchAll = path.join(pagesDir, '[...slug].zen')
464
+ if (fs.existsSync(rootCatchAll)) return rootCatchAll
465
+
466
+ return null
467
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @zenithbuild/cli - Command Registry
3
+ *
4
+ * Central registry for all CLI commands
5
+ */
6
+
7
+ import { dev, type DevOptions } from './dev'
8
+ import { preview, type PreviewOptions } from './preview'
9
+ import { build, type BuildOptions } from './build'
10
+ import { add, type AddOptions } from './add'
11
+ import { remove } from './remove'
12
+ import { create } from './create'
13
+ import * as logger from '../utils/logger'
14
+
15
+ export interface Command {
16
+ name: string
17
+ description: string
18
+ usage: string
19
+ run: (args: string[], options: Record<string, string>) => Promise<void>
20
+ }
21
+
22
+ export const commands: Command[] = [
23
+ {
24
+ name: 'create',
25
+ description: 'Create a new Zenith project',
26
+ usage: 'zenith create [project-name]',
27
+ async run(args) {
28
+ const projectName = args[0]
29
+ await create(projectName)
30
+ }
31
+ },
32
+ {
33
+ name: 'dev',
34
+ description: 'Start development server',
35
+ usage: 'zenith dev [--port <port>]',
36
+ async run(args, options) {
37
+ const opts: DevOptions = {}
38
+ if (options.port) opts.port = parseInt(options.port, 10)
39
+ await dev(opts)
40
+ }
41
+ },
42
+ {
43
+ name: 'preview',
44
+ description: 'Preview production build',
45
+ usage: 'zenith preview [--port <port>]',
46
+ async run(args, options) {
47
+ const opts: PreviewOptions = {}
48
+ if (options.port) opts.port = parseInt(options.port, 10)
49
+ await preview(opts)
50
+ }
51
+ },
52
+ {
53
+ name: 'build',
54
+ description: 'Build for production',
55
+ usage: 'zenith build [--outDir <dir>]',
56
+ async run(args, options) {
57
+ const opts: BuildOptions = {}
58
+ if (options.outDir) opts.outDir = options.outDir
59
+ await build(opts)
60
+ }
61
+ },
62
+ {
63
+ name: 'add',
64
+ description: 'Add a plugin',
65
+ usage: 'zenith add <plugin>',
66
+ async run(args) {
67
+ const pluginName = args[0]
68
+ if (!pluginName) {
69
+ logger.error('Plugin name required')
70
+ process.exit(1)
71
+ }
72
+ await add(pluginName)
73
+ }
74
+ },
75
+ {
76
+ name: 'remove',
77
+ description: 'Remove a plugin',
78
+ usage: 'zenith remove <plugin>',
79
+ async run(args) {
80
+ const pluginName = args[0]
81
+ if (!pluginName) {
82
+ logger.error('Plugin name required')
83
+ process.exit(1)
84
+ }
85
+ await remove(pluginName)
86
+ }
87
+ }
88
+ ]
89
+
90
+ // Placeholder commands for future expansion
91
+ export const placeholderCommands = ['test', 'export', 'deploy']
92
+
93
+ export function getCommand(name: string): Command | undefined {
94
+ return commands.find(c => c.name === name)
95
+ }
96
+
97
+ export function showHelp(): void {
98
+ logger.header('Zenith CLI')
99
+ console.log('Usage: zenith <command> [options]\n')
100
+ console.log('Commands:')
101
+
102
+ for (const cmd of commands) {
103
+ console.log(` ${cmd.name.padEnd(12)} ${cmd.description}`)
104
+ }
105
+
106
+ console.log('\nComing soon:')
107
+ for (const cmd of placeholderCommands) {
108
+ console.log(` ${cmd.padEnd(12)} (not yet implemented)`)
109
+ }
110
+
111
+ console.log('\nRun `zenith <command> --help` for command-specific help.')
112
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @zenithbuild/cli - Preview Command
3
+ *
4
+ * Serves the production build from the distribution directory.
5
+ */
6
+
7
+ import path from 'path'
8
+ import { serve } from 'bun'
9
+ import { requireProject } from '../utils/project'
10
+ import * as logger from '../utils/logger'
11
+
12
+ export interface PreviewOptions {
13
+ port?: number
14
+ }
15
+
16
+ export async function preview(options: PreviewOptions = {}): Promise<void> {
17
+ const project = requireProject()
18
+ const distDir = project.distDir
19
+ const port = options.port || parseInt(process.env.PORT || '4173', 10)
20
+
21
+ logger.header('Zenith Preview Server')
22
+ logger.log(`Serving: ${distDir}`)
23
+
24
+ // File extensions that should be served as static assets
25
+ const STATIC_EXTENSIONS = new Set([
26
+ '.js', '.css', '.ico', '.png', '.jpg', '.jpeg', '.gif', '.svg',
27
+ '.webp', '.woff', '.woff2', '.ttf', '.eot', '.json', '.map'
28
+ ])
29
+
30
+ const server = serve({
31
+ port,
32
+ async fetch(req) {
33
+ const url = new URL(req.url)
34
+ const pathname = url.pathname
35
+ const ext = path.extname(pathname).toLowerCase()
36
+
37
+ if (STATIC_EXTENSIONS.has(ext)) {
38
+ const filePath = path.join(distDir, pathname)
39
+ const file = Bun.file(filePath)
40
+ if (await file.exists()) {
41
+ return new Response(file)
42
+ }
43
+ return new Response('Not found', { status: 404 })
44
+ }
45
+
46
+ const indexPath = path.join(distDir, 'index.html')
47
+ const indexFile = Bun.file(indexPath)
48
+ if (await indexFile.exists()) {
49
+ return new Response(indexFile, {
50
+ headers: { 'Content-Type': 'text/html; charset=utf-8' }
51
+ })
52
+ }
53
+
54
+ return new Response('No production build found. Run `zenith build` first.', { status: 500 })
55
+ }
56
+ })
57
+
58
+ logger.success(`Preview server running at http://localhost:${server.port}`)
59
+ logger.info('Press Ctrl+C to stop')
60
+
61
+ await new Promise(() => { })
62
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @zenithbuild/cli - Remove Command
3
+ *
4
+ * Removes a plugin from the project registry
5
+ */
6
+
7
+ import { requireProject } from '../utils/project'
8
+ import { removePlugin, hasPlugin } from '../utils/plugin-manager'
9
+ import * as logger from '../utils/logger'
10
+
11
+ export async function remove(pluginName: string): Promise<void> {
12
+ requireProject()
13
+
14
+ logger.header('Remove Plugin')
15
+
16
+ if (!pluginName) {
17
+ logger.error('Plugin name required. Usage: zenith remove <plugin>')
18
+ process.exit(1)
19
+ }
20
+
21
+ if (!hasPlugin(pluginName)) {
22
+ logger.warn(`Plugin "${pluginName}" is not registered`)
23
+ return
24
+ }
25
+
26
+ const success = removePlugin(pluginName)
27
+
28
+ if (success) {
29
+ logger.info(`Plugin "${pluginName}" has been unregistered.`)
30
+ logger.info('Note: You may want to remove the package manually:')
31
+ logger.log(` bun remove @zenithbuild/plugin-${pluginName}`)
32
+ }
33
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @zenithbuild/cli
3
+ *
4
+ * CLI for Zenith framework - dev server, build tools, and plugin management
5
+ */
6
+
7
+ export * from './commands/index'
8
+ export * from './utils/logger'
9
+ export * from './utils/project'
10
+ export * from './utils/plugin-manager'