@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.
package/src/main.ts ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @zenithbuild/cli - Shared CLI Execution Logic
3
+ */
4
+
5
+ import process from 'node:process'
6
+ import { getCommand, showHelp, placeholderCommands } from './commands/index'
7
+ import * as logger from './utils/logger'
8
+ import { execSync } from 'node:child_process'
9
+
10
+ /**
11
+ * Check if Bun is available in the environment
12
+ */
13
+ function checkBun() {
14
+ try {
15
+ execSync('bun --version', { stdio: 'pipe' })
16
+ return true
17
+ } catch {
18
+ return false
19
+ }
20
+ }
21
+
22
+ export interface CLIOptions {
23
+ defaultCommand?: string
24
+ }
25
+
26
+ /**
27
+ * Main CLI execution entry point
28
+ */
29
+ export async function runCLI(options: CLIOptions = {}) {
30
+ // 1. Check for Bun
31
+ if (!checkBun()) {
32
+ logger.error('Bun is required to run Zenith.')
33
+ logger.info('Please install Bun: https://bun.sh/install')
34
+ process.exit(1)
35
+ }
36
+
37
+ const args = process.argv.slice(2)
38
+ const VERSION = '0.3.0'
39
+
40
+ // 2. Handle global version flag
41
+ if (args.includes('--version') || args.includes('-v')) {
42
+ console.log(`Zenith CLI v${VERSION}`)
43
+ process.exit(0)
44
+ }
45
+
46
+ // Determine command name: either from args or default (for aliases)
47
+ let commandName = args[0]
48
+ let commandArgs = args.slice(1)
49
+
50
+ if (options.defaultCommand) {
51
+ if (!commandName || commandName.startsWith('-')) {
52
+ commandName = options.defaultCommand
53
+ commandArgs = args
54
+ }
55
+ }
56
+
57
+ // Handle help
58
+ if (!commandName || ((commandName === '--help' || commandName === '-h') && !options.defaultCommand)) {
59
+ showHelp()
60
+ process.exit(0)
61
+ }
62
+
63
+ // Parse options (--key value format) for internal use if needed
64
+ const cliOptions: Record<string, string> = {}
65
+ for (let i = 0; i < commandArgs.length; i++) {
66
+ const arg = commandArgs[i]!
67
+ if (arg.startsWith('--')) {
68
+ const key = arg.slice(2)
69
+ const value = commandArgs[i + 1]
70
+ if (value && !value.startsWith('--')) {
71
+ cliOptions[key] = value
72
+ i++
73
+ } else {
74
+ cliOptions[key] = 'true'
75
+ }
76
+ }
77
+ }
78
+
79
+ // Check for placeholder commands
80
+ if (placeholderCommands.includes(commandName)) {
81
+ logger.warn(`Command "${commandName}" is not yet implemented.`)
82
+ logger.info('This feature is planned for a future release.')
83
+ process.exit(0)
84
+ }
85
+
86
+ const command = getCommand(commandName)
87
+
88
+ if (!command) {
89
+ logger.error(`Unknown command: ${commandName}`)
90
+ showHelp()
91
+ process.exit(1)
92
+ }
93
+
94
+ try {
95
+ await command!.run(commandArgs, cliOptions)
96
+ } catch (err: unknown) {
97
+ const message = err instanceof Error ? err.message : String(err)
98
+ logger.error(message)
99
+ process.exit(1)
100
+ }
101
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Zenith CLI Branding
3
+ *
4
+ * ASCII art logo, colors, animations, and styled output
5
+ */
6
+
7
+ import pc from 'picocolors'
8
+
9
+ // Brand colors
10
+ export const colors = {
11
+ primary: pc.blue,
12
+ secondary: pc.cyan,
13
+ success: pc.green,
14
+ warning: pc.yellow,
15
+ error: pc.red,
16
+ muted: pc.gray,
17
+ bold: pc.bold,
18
+ dim: pc.dim
19
+ }
20
+
21
+ // ASCII Zenith logo
22
+ export const LOGO = `
23
+ ${pc.cyan('╔═══════════════════════════════════════════════════════════╗')}
24
+ ${pc.cyan('║')} ${pc.cyan('║')}
25
+ ${pc.cyan('║')} ${pc.bold(pc.blue('███████╗'))}${pc.bold(pc.cyan('███████╗'))}${pc.bold(pc.blue('███╗ ██╗'))}${pc.bold(pc.cyan('██╗'))}${pc.bold(pc.blue('████████╗'))}${pc.bold(pc.cyan('██╗ ██╗'))} ${pc.cyan('║')}
26
+ ${pc.cyan('║')} ${pc.bold(pc.blue('╚══███╔╝'))}${pc.bold(pc.cyan('██╔════╝'))}${pc.bold(pc.blue('████╗ ██║'))}${pc.bold(pc.cyan('██║'))}${pc.bold(pc.blue('╚══██╔══╝'))}${pc.bold(pc.cyan('██║ ██║'))} ${pc.cyan('║')}
27
+ ${pc.cyan('║')} ${pc.bold(pc.blue(' ███╔╝ '))}${pc.bold(pc.cyan('█████╗ '))}${pc.bold(pc.blue('██╔██╗ ██║'))}${pc.bold(pc.cyan('██║'))}${pc.bold(pc.blue(' ██║ '))}${pc.bold(pc.cyan('███████║'))} ${pc.cyan('║')}
28
+ ${pc.cyan('║')} ${pc.bold(pc.blue(' ███╔╝ '))}${pc.bold(pc.cyan('██╔══╝ '))}${pc.bold(pc.blue('██║╚██╗██║'))}${pc.bold(pc.cyan('██║'))}${pc.bold(pc.blue(' ██║ '))}${pc.bold(pc.cyan('██╔══██║'))} ${pc.cyan('║')}
29
+ ${pc.cyan('║')} ${pc.bold(pc.blue('███████╗'))}${pc.bold(pc.cyan('███████╗'))}${pc.bold(pc.blue('██║ ╚████║'))}${pc.bold(pc.cyan('██║'))}${pc.bold(pc.blue(' ██║ '))}${pc.bold(pc.cyan('██║ ██║'))} ${pc.cyan('║')}
30
+ ${pc.cyan('║')} ${pc.bold(pc.blue('╚══════╝'))}${pc.bold(pc.cyan('╚══════╝'))}${pc.bold(pc.blue('╚═╝ ╚═══╝'))}${pc.bold(pc.cyan('╚═╝'))}${pc.bold(pc.blue(' ╚═╝ '))}${pc.bold(pc.cyan('╚═╝ ╚═╝'))} ${pc.cyan('║')}
31
+ ${pc.cyan('║')} ${pc.cyan('║')}
32
+ ${pc.cyan('║')} ${pc.dim('The Modern Reactive Web Framework')} ${pc.cyan('║')}
33
+ ${pc.cyan('║')} ${pc.cyan('║')}
34
+ ${pc.cyan('╚═══════════════════════════════════════════════════════════╝')}
35
+ `
36
+
37
+ // Compact logo for smaller spaces
38
+ export const LOGO_COMPACT = `
39
+ ${pc.bold(pc.blue('⚡'))} ${pc.bold(pc.cyan('ZENITH'))} ${pc.dim('- Modern Reactive Framework')}
40
+ `
41
+
42
+ // Spinner frames for animations
43
+ const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
44
+
45
+ export class Spinner {
46
+ private interval: ReturnType<typeof setInterval> | null = null
47
+ private frameIndex = 0
48
+ private message: string
49
+
50
+ constructor(message: string) {
51
+ this.message = message
52
+ }
53
+
54
+ start() {
55
+ this.interval = setInterval(() => {
56
+ process.stdout.write(`\r${pc.cyan(spinnerFrames[this.frameIndex])} ${this.message}`)
57
+ this.frameIndex = (this.frameIndex + 1) % spinnerFrames.length
58
+ }, 80)
59
+ }
60
+
61
+ stop(finalMessage?: string) {
62
+ if (this.interval) {
63
+ clearInterval(this.interval)
64
+ this.interval = null
65
+ }
66
+ process.stdout.write('\r' + ' '.repeat(this.message.length + 5) + '\r')
67
+ if (finalMessage) {
68
+ console.log(finalMessage)
69
+ }
70
+ }
71
+
72
+ succeed(message: string) {
73
+ this.stop(`${pc.green('✓')} ${message}`)
74
+ }
75
+
76
+ fail(message: string) {
77
+ this.stop(`${pc.red('✗')} ${message}`)
78
+ }
79
+ }
80
+
81
+ // Styled output functions
82
+ export function showLogo() {
83
+ console.log(LOGO)
84
+ }
85
+
86
+ export function showCompactLogo() {
87
+ console.log(LOGO_COMPACT)
88
+ }
89
+
90
+ export function header(text: string) {
91
+ console.log(`\n${pc.bold(pc.cyan('▸'))} ${pc.bold(text)}\n`)
92
+ }
93
+
94
+ export function success(text: string) {
95
+ console.log(`${pc.green('✓')} ${text}`)
96
+ }
97
+
98
+ export function error(text: string) {
99
+ console.log(`${pc.red('✗')} ${text}`)
100
+ }
101
+
102
+ export function warn(text: string) {
103
+ console.log(`${pc.yellow('⚠')} ${text}`)
104
+ }
105
+
106
+ export function info(text: string) {
107
+ console.log(`${pc.blue('ℹ')} ${text}`)
108
+ }
109
+
110
+ export function step(num: number, text: string) {
111
+ console.log(`${pc.dim(`[${num}]`)} ${text}`)
112
+ }
113
+
114
+ export function highlight(text: string): string {
115
+ return pc.cyan(text)
116
+ }
117
+
118
+ export function dim(text: string): string {
119
+ return pc.dim(text)
120
+ }
121
+
122
+ export function bold(text: string): string {
123
+ return pc.bold(text)
124
+ }
125
+
126
+ // Animated intro (optional)
127
+ export async function showIntro() {
128
+ console.clear()
129
+ showLogo()
130
+ await sleep(300)
131
+ }
132
+
133
+ function sleep(ms: number): Promise<void> {
134
+ return new Promise(resolve => setTimeout(resolve, ms))
135
+ }
136
+
137
+ // Next steps box
138
+ export function showNextSteps(projectName: string) {
139
+ console.log(`
140
+ ${pc.cyan('┌─────────────────────────────────────────────────────────┐')}
141
+ ${pc.cyan('│')} ${pc.cyan('│')}
142
+ ${pc.cyan('│')} ${pc.green('✨')} ${pc.bold('Your Zenith app is ready!')} ${pc.cyan('│')}
143
+ ${pc.cyan('│')} ${pc.cyan('│')}
144
+ ${pc.cyan('│')} ${pc.dim('Next steps:')} ${pc.cyan('│')}
145
+ ${pc.cyan('│')} ${pc.cyan('│')}
146
+ ${pc.cyan('│')} ${pc.cyan('$')} ${pc.bold(`cd ${projectName}`)}${' '.repeat(Math.max(0, 40 - projectName.length))}${pc.cyan('│')}
147
+ ${pc.cyan('│')} ${pc.cyan('$')} ${pc.bold('bun run dev')} ${pc.cyan('│')}
148
+ ${pc.cyan('│')} ${pc.cyan('│')}
149
+ ${pc.cyan('│')} ${pc.dim('Then open')} ${pc.underline(pc.blue('http://localhost:3000'))} ${pc.cyan('│')}
150
+ ${pc.cyan('│')} ${pc.cyan('│')}
151
+ ${pc.cyan('└─────────────────────────────────────────────────────────┘')}
152
+ `)
153
+ }
154
+
155
+ /**
156
+ * Show dev server startup panel
157
+ */
158
+ export function showServerPanel(options: {
159
+ project: string,
160
+ pages: string,
161
+ url: string,
162
+ hmr: boolean,
163
+ mode: string
164
+ }) {
165
+ console.clear()
166
+ console.log(LOGO_COMPACT)
167
+ console.log(`${pc.cyan('────────────────────────────────────────────────────────────')}`)
168
+ console.log(` ${pc.magenta('🟣 Zenith Dev Server')}`)
169
+ console.log(`${pc.cyan('────────────────────────────────────────────────────────────')}`)
170
+ console.log(` ${pc.bold('Project:')} ${pc.dim(options.project)}`)
171
+ console.log(` ${pc.bold('Pages:')} ${pc.dim(options.pages)}`)
172
+ console.log(` ${pc.bold('Mode:')} ${pc.cyan(options.mode)} ${pc.dim(`(${options.hmr ? 'HMR enabled' : 'HMR disabled'})`)}`)
173
+ console.log(`${pc.cyan('────────────────────────────────────────────────────────────')}`)
174
+ console.log(` ${pc.bold('Server:')} ${pc.cyan(pc.underline(options.url))} ${pc.dim('(clickable)')}`)
175
+ console.log(` ${pc.bold('Hot Reload:')} ${options.hmr ? pc.green('Enabled ✅') : pc.red('Disabled ✗')}`)
176
+ console.log(`${pc.cyan('────────────────────────────────────────────────────────────')}`)
177
+ console.log(` ${pc.dim('Press Ctrl+C to stop')}\n`)
178
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @zenithbuild/cli - Logger Utility
3
+ *
4
+ * Colored console output for CLI feedback
5
+ */
6
+
7
+ import pc from 'picocolors'
8
+
9
+ export function log(message: string): void {
10
+ console.log(`${pc.cyan('[zenith]')} ${message}`)
11
+ }
12
+
13
+ export function success(message: string): void {
14
+ console.log(`${pc.green('✓')} ${message}`)
15
+ }
16
+
17
+ export function warn(message: string): void {
18
+ console.log(`${pc.yellow('⚠')} ${message}`)
19
+ }
20
+
21
+ export function error(message: string): void {
22
+ console.error(`${pc.red('✗')} ${message}`)
23
+ }
24
+
25
+ export function info(message: string): void {
26
+ console.log(`${pc.blue('ℹ')} ${message}`)
27
+ }
28
+
29
+ export function header(title: string): void {
30
+ console.log(`\n${pc.bold(pc.cyan(title))}\n`)
31
+ }
32
+
33
+ export function hmr(type: 'CSS' | 'Page' | 'Layout' | 'Content', path: string): void {
34
+ console.log(`${pc.magenta('[HMR]')} ${pc.bold(type)} updated: ${pc.dim(path)}`)
35
+ }
36
+
37
+ export function route(method: string, path: string, status: number, totalMs: number, compileMs: number, renderMs: number): void {
38
+ const statusColor = status < 400 ? pc.green : pc.red
39
+ const timeColor = totalMs > 1000 ? pc.yellow : pc.gray
40
+
41
+ console.log(
42
+ `${pc.bold(method)} ${pc.cyan(path.padEnd(15))} ` +
43
+ `${statusColor(status)} ${pc.dim('in')} ${timeColor(`${totalMs}ms`)} ` +
44
+ `${pc.dim(`(compile: ${compileMs}ms, render: ${renderMs}ms)`)}`
45
+ )
46
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @zenithbuild/cli - Plugin Manager
3
+ *
4
+ * Manages zenith.plugins.json for plugin registration
5
+ */
6
+
7
+ import fs from 'fs'
8
+ import path from 'path'
9
+ import { findProjectRoot } from './project'
10
+ import * as logger from './logger'
11
+
12
+ export interface PluginConfig {
13
+ name: string
14
+ installedAt: string
15
+ options?: Record<string, unknown>
16
+ }
17
+
18
+ export interface PluginsFile {
19
+ plugins: PluginConfig[]
20
+ }
21
+
22
+ const PLUGINS_FILE = 'zenith.plugins.json'
23
+
24
+ /**
25
+ * Get path to plugins file
26
+ */
27
+ function getPluginsPath(): string {
28
+ const root = findProjectRoot()
29
+ if (!root) {
30
+ throw new Error('Not in a Zenith project')
31
+ }
32
+ return path.join(root, PLUGINS_FILE)
33
+ }
34
+
35
+ /**
36
+ * Read plugins file
37
+ */
38
+ export function readPlugins(): PluginsFile {
39
+ const pluginsPath = getPluginsPath()
40
+
41
+ if (!fs.existsSync(pluginsPath)) {
42
+ return { plugins: [] }
43
+ }
44
+
45
+ try {
46
+ return JSON.parse(fs.readFileSync(pluginsPath, 'utf-8'))
47
+ } catch {
48
+ return { plugins: [] }
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Write plugins file
54
+ */
55
+ function writePlugins(data: PluginsFile): void {
56
+ const pluginsPath = getPluginsPath()
57
+ fs.writeFileSync(pluginsPath, JSON.stringify(data, null, 2))
58
+ }
59
+
60
+ /**
61
+ * Add a plugin to the registry
62
+ */
63
+ export function addPlugin(name: string, options?: Record<string, unknown>): boolean {
64
+ const data = readPlugins()
65
+
66
+ // Check if already installed
67
+ if (data.plugins.some(p => p.name === name)) {
68
+ logger.warn(`Plugin "${name}" is already registered`)
69
+ return false
70
+ }
71
+
72
+ data.plugins.push({
73
+ name,
74
+ installedAt: new Date().toISOString(),
75
+ options
76
+ })
77
+
78
+ writePlugins(data)
79
+ logger.success(`Added plugin "${name}"`)
80
+ return true
81
+ }
82
+
83
+ /**
84
+ * Remove a plugin from the registry
85
+ */
86
+ export function removePlugin(name: string): boolean {
87
+ const data = readPlugins()
88
+ const initialLength = data.plugins.length
89
+
90
+ data.plugins = data.plugins.filter(p => p.name !== name)
91
+
92
+ if (data.plugins.length === initialLength) {
93
+ logger.warn(`Plugin "${name}" is not registered`)
94
+ return false
95
+ }
96
+
97
+ writePlugins(data)
98
+ logger.success(`Removed plugin "${name}"`)
99
+ return true
100
+ }
101
+
102
+ /**
103
+ * List all registered plugins
104
+ */
105
+ export function listPlugins(): PluginConfig[] {
106
+ return readPlugins().plugins
107
+ }
108
+
109
+ /**
110
+ * Check if a plugin is registered
111
+ */
112
+ export function hasPlugin(name: string): boolean {
113
+ return readPlugins().plugins.some(p => p.name === name)
114
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @zenithbuild/cli - Project Utility
3
+ *
4
+ * Detects Zenith project root and configuration
5
+ */
6
+
7
+ import fs from 'fs'
8
+ import path from 'path'
9
+
10
+ export interface ZenithProject {
11
+ root: string
12
+ pagesDir: string
13
+ distDir: string
14
+ hasZenithDeps: boolean
15
+ }
16
+
17
+ /**
18
+ * Find the project root by looking for package.json with @zenith dependencies
19
+ */
20
+ export function findProjectRoot(startDir: string = process.cwd()): string | null {
21
+ let current = startDir
22
+
23
+ while (current !== path.dirname(current)) {
24
+ const pkgPath = path.join(current, 'package.json')
25
+
26
+ if (fs.existsSync(pkgPath)) {
27
+ try {
28
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
29
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies }
30
+
31
+ // Check for any @zenithbuild/* or @zenithbuild/* dependency
32
+ const hasZenith = Object.keys(deps).some(d => d.startsWith('@zenithbuild/') || d.startsWith('@zenithbuild/'))
33
+ if (hasZenith) {
34
+ return current
35
+ }
36
+ } catch {
37
+ // Invalid JSON, skip
38
+ }
39
+ }
40
+
41
+ current = path.dirname(current)
42
+ }
43
+
44
+ return null
45
+ }
46
+
47
+ /**
48
+ * Get project configuration
49
+ */
50
+ export function getProject(cwd: string = process.cwd()): ZenithProject | null {
51
+ const root = findProjectRoot(cwd)
52
+ if (!root) return null
53
+
54
+ // Support both app/ and src/ directory structures
55
+ let appDir = path.join(root, 'app')
56
+ if (!fs.existsSync(appDir)) {
57
+ appDir = path.join(root, 'src')
58
+ }
59
+
60
+ return {
61
+ root,
62
+ pagesDir: path.join(appDir, 'pages'),
63
+ distDir: path.join(appDir, 'dist'),
64
+ hasZenithDeps: true
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Ensure we're in a Zenith project
70
+ */
71
+ export function requireProject(cwd: string = process.cwd()): ZenithProject {
72
+ const project = getProject(cwd)
73
+ if (!project) {
74
+ throw new Error('Not in a Zenith project. Run this command from a directory with @zenithbuild/* dependencies.')
75
+ }
76
+ return project
77
+ }