jackdaw 1.0.0

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.
data/TODO.md ADDED
@@ -0,0 +1,167 @@
1
+ # Jackdaw - Static Site Generator TODO
2
+
3
+ ## Project Overview
4
+ A fast, incremental static site generator with folder-based organization using Thor for CLI.
5
+
6
+ ---
7
+
8
+ ## Phase 1: Project Setup
9
+ - [x] Add Thor gem dependency to gemspec
10
+ - [x] Add development dependencies (rspec, rubocop, etc.)
11
+ - [x] Create basic CLI structure in exe/jackdaw
12
+ - [x] Set up Thor CLI class structure
13
+ - [x] Add any additional dependencies (for markdown parsing, file watching, etc.)
14
+
15
+ ## Phase 2: Core File System Structure
16
+ - [x] Define standard project structure (site/ directory)
17
+ - [x] Create file watcher/tracker for incremental builds
18
+ - [x] Implement directory scanner
19
+ - [x] Create file type registry (.page, .post, etc.)
20
+ - [x] Build metadata extractor from file structure (path, name, dates)
21
+ - [x] Extract metadata from first heading as title (H1)
22
+ - [x] Implement content type handlers
23
+ - [x] Page handler
24
+ - [x] Post handler
25
+ - [x] Asset handler
26
+
27
+ ## Phase 3: Template System
28
+ - [x] Design template engine integration (ERB)
29
+ - [x] Implement template loader from templates/ directory
30
+ - [x] Create template context builder
31
+ - [x] Add layout/partial support
32
+ - [x] Implement template caching for performance
33
+
34
+ ## Phase 4: Content Processing
35
+ - [x] Implement markdown parser integration
36
+ - [x] Create content preprocessor pipeline
37
+ - [x] Extract metadata from filename (dates, slugs)
38
+ - [x] Extract title from first H1 in content
39
+ - [x] Derive metadata from folder structure and hierarchy
40
+ - [x] Implement content transformation pipeline
41
+ - [x] Add syntax highlighting support (Rouge)
42
+
43
+ ## Phase 5: Build System
44
+ - [x] Design dependency graph for incremental builds
45
+ - [x] Implement file change detection
46
+ - [x] Create build cache system
47
+ - [x] Build output directory management (public/)
48
+ - [x] Implement parallel processing for speed
49
+ - [x] Add build statistics/reporting
50
+ - [x] Create clean build option
51
+
52
+ ## Phase 6: CLI Commands - Build
53
+ - [x] Implement `jackdaw build` command
54
+ - [x] Full build option
55
+ - [x] Incremental build (default)
56
+ - [x] Watch mode option (deferred to serve command)
57
+ - [x] Verbose/debug output
58
+ - [x] Clean option
59
+ - [x] Add build configuration loading (skipped - convention over configuration)
60
+ - [x] Implement error handling and reporting
61
+
62
+ ## Phase 7: CLI Commands - Serve
63
+ - [x] Implement `jackdaw serve` command
64
+ - [x] Choose and integrate web server (Puma with Rack)
65
+ - [x] Add live reload support (polling-based with JavaScript)
66
+ - [x] Implement file watching for auto-rebuild (Listen gem)
67
+ - [x] Configure port and host options
68
+ - [x] Add middleware for development (StaticFileServer + LiveReloadMiddleware)
69
+ - [x] Beautiful CLI output with rebuild stats
70
+ - [x] Graceful shutdown handling (Ctrl+C)
71
+
72
+ ## Phase 8: CLI Commands - Create
73
+ - [x] Implement `jackdaw create <template> <name>` command
74
+ - [x] Create template scaffolding system
75
+ - [x] Add default built-in templates:
76
+ - [x] `page` template (basic page.html.erb)
77
+ - [x] `blog` template (blog post with date handling)
78
+ - [x] Support arbitrary user-defined templates in templates/ directory
79
+ - [x] Discover available templates from templates/ folder
80
+ - [x] Add date/timestamp handling for posts (auto-prefix for blog/post/article/news)
81
+ - [x] Allow users to add custom templates by creating template files
82
+ - [x] Implement `jackdaw template list` command
83
+ - [x] Add --dated and --no-date flags for override control
84
+ - [x] Support nested paths (e.g., `jackdaw create page company/about`)
85
+ - [x] Validate template existence before creating files
86
+ - [x] Add Builder warnings for missing templates
87
+
88
+ ## Phase 9: Configuration System
89
+ - [x] Skipped - Maintaining config-less, convention-over-configuration approach
90
+
91
+ ## Phase 10: Asset Handling
92
+ - [x] Implement asset copying
93
+ - [x] Keeping minimal - assets copied as-is for speed and simplicity
94
+
95
+ ## Phase 11: Performance Optimization
96
+ - [x] Profile build performance
97
+ - [x] Implement multi-threading for file processing (Parallel gem)
98
+ - [x] Optimize file I/O operations (incremental builds with mtime checks)
99
+ - [ ] Add memory usage optimization
100
+ - [x] Benchmark against common static site generators
101
+ - Small (30 files): 164 files/sec clean, 5821 files/sec incremental
102
+ - Medium (150 files): 428 files/sec clean, 12343 files/sec incremental
103
+ - Large (600 files): 693 files/sec clean, 16280 files/sec incremental
104
+ - Incremental builds: 6-18x faster than clean builds
105
+ - [x] Cache template compilation (ERB caching in Renderer)
106
+ - [x] Optimize dependency graph traversal (mtime-based change detection)
107
+
108
+ ## Phase 12: Documentation
109
+ - [x] Write comprehensive README with quickstart guide
110
+ - [x] Document CLI commands and options (in README)
111
+ - [x] Create project structure guide (in README)
112
+ - [x] Write template documentation (in README)
113
+ - [x] Add configuration reference (N/A - convention-based, no config needed)
114
+ - [x] Add troubleshooting guide (in README)
115
+ - [x] Consolidate all require statements to jackdaw.rb
116
+ - [x] Add comprehensive code comments to jackdaw.rb and project.rb
117
+ - [ ] Add code comments to remaining core files
118
+ - [ ] Create example site (basic example already created by `jackdaw new`)
119
+
120
+ ## Phase 13: Testing
121
+ - [x] Unit tests for core components (Project, FileTypes, Scanner)
122
+ - [x] Unit tests for Renderer (21 tests)
123
+ - [x] Integration tests for Builder (18 tests)
124
+ - [x] End-to-end build tests (10 comprehensive workflow tests)
125
+ - [x] Test helper infrastructure with fixtures
126
+ - [x] Performance benchmarks (completed in Phase 11)
127
+ - **Total: 99 passing tests, 0 failures**
128
+
129
+ ## Phase 14: Polish & Release
130
+ - [x] Code cleanup and refactoring (reduced complexity in Build command)
131
+ - [x] RuboCop compliance (all files passing, 0 offenses)
132
+ - [x] Error message improvements (beautiful CLI output with colors)
133
+ - [x] Add progress indicators (build stats, rebuild notifications)
134
+ - [x] Version 1.0.0 release preparation
135
+ - [x] Updated version to 1.0.0
136
+ - [x] Created comprehensive CHANGELOG.md
137
+ - [x] Updated gemspec with proper metadata
138
+ - [x] All 99 tests passing
139
+ - [ ] Publish to RubyGems (deferred)
140
+
141
+ ## Phase 15: Essential Web Features
142
+ - [ ] RSS/Atom feed generation (for blog posts)
143
+ - [ ] Sitemap.xml generation (for SEO)
144
+ - [ ] SEO meta tag helpers (Open Graph, Twitter Cards)
145
+
146
+ ---
147
+
148
+ ## Nice-to-Have Features (Future)
149
+ - [ ] Search index generation (JSON for client-side search)
150
+ - [ ] Git integration (simple deploy command wrapper)
151
+
152
+ ## Won't Implement (Against Project Philosophy)
153
+ - ~~Multiple template engine support~~ - ERB is sufficient, adds complexity
154
+ - ~~Image lazy loading helpers~~ - Can be done in templates/CSS
155
+ - ~~i18n/l10n support~~ - Too complex, users can handle with folder structure
156
+ - ~~CloudFlare/Netlify deployment helpers~~ - Too specific, manual deploy is simple enough
157
+
158
+ ---
159
+
160
+ ## Technical Decisions to Make
161
+ - [x] Template engine: ERB (built-in, fast, zero dependencies)
162
+ - [x] Markdown parser: Kramdown (pure Ruby, no compilation issues)
163
+ - [x] Server: Puma (fast, industry standard)
164
+ - [x] File watching: Listen gem (reliable, cross-platform)
165
+ - [ ] Config format: YAML vs TOML vs Ruby DSL? (may skip - convention over configuration)
166
+ - [x] Incremental build strategy: Timestamp-based (mtime checks)
167
+ - [x] Metadata approach: Convention-based (NO frontmatter, derive from structure/content)
data/benchmark.rb ADDED
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'benchmark'
6
+ require 'fileutils'
7
+ require_relative 'lib/jackdaw'
8
+
9
+ # Benchmark Jackdaw performance
10
+ class JackdawBenchmark
11
+ BENCHMARK_DIR = 'benchmark-site.site'
12
+ SIZES = {
13
+ small: { pages: 10, blog_posts: 20 },
14
+ medium: { pages: 50, blog_posts: 100 },
15
+ large: { pages: 100, blog_posts: 500 }
16
+ }.freeze
17
+
18
+ def initialize(size: :medium)
19
+ @size = size
20
+ @config = SIZES[size]
21
+ @project = Jackdaw::Project.new(BENCHMARK_DIR)
22
+ end
23
+
24
+ def run
25
+ puts "\n#{colorize('🚀 Jackdaw Performance Benchmark', :bold, :magenta)}"
26
+ puts colorize("Site size: #{@size} (#{@config[:pages]} pages, #{@config[:blog_posts]} blog posts)", :cyan)
27
+ puts colorize('=' * 70, :cyan)
28
+
29
+ cleanup
30
+ create_test_site
31
+ run_benchmarks
32
+ cleanup
33
+
34
+ puts "\n#{colorize('✓ Benchmark complete!', :green)}"
35
+ end
36
+
37
+ private
38
+
39
+ def create_test_site
40
+ print colorize('Creating test site...', :yellow)
41
+ @project.create!
42
+ create_templates
43
+ create_content
44
+ puts colorize(' Done!', :green)
45
+ end
46
+
47
+ def create_templates
48
+ # Layout
49
+ File.write(File.join(@project.templates_dir, 'layout.html.erb'), <<~ERB)
50
+ <!DOCTYPE html>
51
+ <html lang="en">
52
+ <head>
53
+ <meta charset="UTF-8">
54
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
55
+ <title><%= title %> - Benchmark Site</title>
56
+ <style>
57
+ body { max-width: 800px; margin: 0 auto; padding: 2rem; font-family: system-ui; line-height: 1.6; }
58
+ nav { margin-bottom: 2rem; }
59
+ </style>
60
+ </head>
61
+ <body>
62
+ <%= content %>
63
+ </body>
64
+ </html>
65
+ ERB
66
+
67
+ # Page template
68
+ File.write(File.join(@project.templates_dir, 'page.html.erb'), <<~ERB)
69
+ <main>
70
+ <%= content %>
71
+ </main>
72
+ ERB
73
+
74
+ # Blog template
75
+ File.write(File.join(@project.templates_dir, 'blog.html.erb'), <<~ERB)
76
+ <article>
77
+ <header>
78
+ <h1><%= title %></h1>
79
+ <time datetime="<%= date %>"><%= date.strftime('%B %d, %Y') %></time>
80
+ </header>
81
+ <%= content %>
82
+ </article>
83
+ ERB
84
+ end
85
+
86
+ def create_content
87
+ # Create pages
88
+ @config[:pages].times do |i|
89
+ File.write(File.join(@project.src_dir, "page-#{i}.page.md"), generate_page_content(i))
90
+ end
91
+
92
+ # Create blog posts
93
+ blog_dir = File.join(@project.src_dir, 'blog')
94
+ FileUtils.mkdir_p(blog_dir)
95
+ @config[:blog_posts].times do |i|
96
+ date = Date.today - i
97
+ File.write(File.join(blog_dir, "#{date}-post-#{i}.blog.md"), generate_blog_content(i))
98
+ end
99
+ end
100
+
101
+ def generate_page_content(index)
102
+ <<~MD
103
+ # Page #{index}
104
+
105
+ This is test page number #{index} for benchmarking Jackdaw performance.
106
+
107
+ ## Section 1
108
+
109
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
110
+
111
+ ## Section 2
112
+
113
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
114
+
115
+ ### Subsection
116
+
117
+ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
118
+
119
+ ## Code Example
120
+
121
+ ```ruby
122
+ def hello_world
123
+ puts "Hello from page #{index}!"
124
+ end
125
+ ```
126
+
127
+ **Bold text** and *italic text* and `code snippets`.
128
+ MD
129
+ end
130
+
131
+ def generate_blog_content(index)
132
+ <<~MD
133
+ # Blog Post #{index}
134
+
135
+ This is test blog post number #{index} for benchmarking.
136
+
137
+ ## Introduction
138
+
139
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero.
140
+
141
+ ## Main Content
142
+
143
+ Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet.
144
+
145
+ ```javascript
146
+ function testFunction() {
147
+ console.log('Test from post #{index}');
148
+ return true;
149
+ }
150
+ ```
151
+
152
+ ## Conclusion
153
+
154
+ Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta.
155
+
156
+ - List item 1
157
+ - List item 2
158
+ - List item 3
159
+ MD
160
+ end
161
+
162
+ def run_benchmarks
163
+ puts "\n#{colorize('Running benchmarks...', :bold)}\n"
164
+
165
+ # Clean build
166
+ clean_build_time = benchmark_build(clean: true)
167
+ total_files = @config[:pages] + @config[:blog_posts]
168
+
169
+ # Incremental build (no changes)
170
+ incremental_build_time = benchmark_build(clean: false)
171
+
172
+ # Incremental build (with change)
173
+ modify_single_file
174
+ incremental_change_time = benchmark_build(clean: false)
175
+
176
+ # Results
177
+ puts "\n#{colorize('Results:', :bold, :magenta)}"
178
+ puts colorize('-' * 70, :cyan)
179
+
180
+ puts "Total files: #{colorize(total_files.to_s, :cyan)}"
181
+ puts "Clean build time: #{colorize(format('%.3f seconds', clean_build_time), :green)}"
182
+ puts " Files/second: #{colorize(format('%.1f', total_files / clean_build_time), :cyan)}"
183
+ puts "Incremental (no change): #{colorize(format('%.3f seconds', incremental_build_time), :green)}"
184
+ puts " Files/second: #{colorize(format('%.1f', total_files / incremental_build_time), :cyan)}"
185
+ puts "Incremental (1 change): #{colorize(format('%.3f seconds', incremental_change_time), :green)}"
186
+ puts " Speedup vs clean: #{colorize(format('%.1fx', clean_build_time / incremental_change_time), :yellow)}"
187
+
188
+ puts colorize('-' * 70, :cyan)
189
+ end
190
+
191
+ def benchmark_build(clean: false)
192
+ builder = Jackdaw::Builder.new(@project, { clean: clean })
193
+
194
+ time = Benchmark.realtime do
195
+ builder.build
196
+ end
197
+
198
+ label = clean ? 'Clean build' : 'Incremental build'
199
+ puts " #{colorize(label, :yellow)}: #{colorize(format('%.3fs', time), :green)}"
200
+
201
+ time
202
+ end
203
+
204
+ def modify_single_file
205
+ # Touch one file to trigger incremental rebuild
206
+ first_page = File.join(@project.src_dir, 'page-0.page.md')
207
+ content = File.read(first_page)
208
+ File.write(first_page, content + "\n\nUpdated content.")
209
+ end
210
+
211
+ def cleanup
212
+ FileUtils.rm_rf(BENCHMARK_DIR) if Dir.exist?(BENCHMARK_DIR)
213
+ end
214
+
215
+ def colorize(text, *colors)
216
+ codes = {
217
+ reset: "\e[0m",
218
+ bold: "\e[1m",
219
+ green: "\e[32m",
220
+ cyan: "\e[36m",
221
+ yellow: "\e[33m",
222
+ magenta: "\e[35m"
223
+ }
224
+
225
+ prefix = colors.map { |c| codes[c] }.join
226
+ "#{prefix}#{text}#{codes[:reset]}"
227
+ end
228
+ end
229
+
230
+ # Run benchmark
231
+ if __FILE__ == $PROGRAM_NAME
232
+ size = ARGV[0]&.to_sym || :medium
233
+ benchmark = JackdawBenchmark.new(size: size)
234
+ benchmark.run
235
+ end
data/exe/jackdaw ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'jackdaw'
5
+
6
+ Jackdaw::CLI.start(ARGV)
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jackdaw
4
+ # Orchestrates the build process with incremental builds and parallel processing
5
+ class Builder
6
+ attr_reader :project, :scanner, :renderer, :stats
7
+
8
+ def initialize(project, options = {})
9
+ @project = project
10
+ @scanner = Scanner.new(project)
11
+ @renderer = Renderer.new(project)
12
+ @options = options
13
+ @stats = BuildStats.new
14
+ end
15
+
16
+ # Run full build
17
+ def build
18
+ start_time = Time.now
19
+
20
+ # Clean if requested
21
+ clean_output if @options[:clean]
22
+
23
+ # Ensure output directory exists
24
+ FileUtils.mkdir_p(project.output_dir)
25
+
26
+ # Build content files in parallel
27
+ build_content_files
28
+
29
+ # Copy assets in parallel
30
+ copy_assets
31
+
32
+ # Generate feeds and sitemap
33
+ generate_feeds_and_sitemap
34
+
35
+ @stats.total_time = Time.now - start_time
36
+ @stats
37
+ end
38
+
39
+ # Clean the output directory
40
+ def clean_output
41
+ return unless Dir.exist?(project.output_dir)
42
+
43
+ FileUtils.rm_rf(Dir.glob(File.join(project.output_dir, '*')))
44
+ end
45
+
46
+ private
47
+
48
+ def build_content_files
49
+ content_files = scanner.content_files
50
+
51
+ # Process files in parallel
52
+ results = Parallel.map(content_files, in_threads: Parallel.processor_count) do |content_file|
53
+ process_content_file(content_file)
54
+ end
55
+
56
+ # Aggregate results
57
+ results.each do |result|
58
+ case result[:status]
59
+ when :built
60
+ @stats.files_built += 1
61
+ when :skipped
62
+ @stats.files_skipped += 1
63
+ when :error
64
+ @stats.errors << result[:error]
65
+ end
66
+ end
67
+ end
68
+
69
+ def process_content_file(content_file)
70
+ # Check if template exists
71
+ template_file = File.join(project.templates_dir, "#{content_file.type}.html.erb")
72
+ unless File.exist?(template_file)
73
+ warning = "⚠️ Missing template '#{content_file.type}.html.erb' for #{content_file.path}"
74
+ puts "\e[33m#{warning}\e[0m" if @options[:verbose]
75
+ return { status: :error, file: content_file.path,
76
+ error: StandardError.new("Missing template: #{content_file.type}.html.erb") }
77
+ end
78
+
79
+ # Check if we need to rebuild
80
+ return { status: :skipped, file: content_file.path } unless needs_rebuild?(content_file)
81
+
82
+ # Render and write
83
+ html = renderer.render_content(content_file)
84
+ output_file = content_file.output_file
85
+
86
+ FileUtils.mkdir_p(File.dirname(output_file))
87
+ File.write(output_file, html)
88
+
89
+ { status: :built, file: content_file.path }
90
+ rescue StandardError => e
91
+ { status: :error, file: content_file.path, error: e }
92
+ end
93
+
94
+ def copy_assets
95
+ asset_files = scanner.asset_files
96
+
97
+ results = Parallel.map(asset_files, in_threads: Parallel.processor_count) do |asset_file|
98
+ next { status: :skipped } unless needs_asset_copy?(asset_file)
99
+
100
+ asset_file.copy!
101
+ { status: :copied }
102
+ rescue StandardError => e
103
+ { status: :error, error: e }
104
+ end
105
+
106
+ results.each do |result|
107
+ case result[:status]
108
+ when :copied
109
+ @stats.assets_copied += 1
110
+ when :skipped
111
+ @stats.assets_skipped += 1
112
+ when :error
113
+ @stats.errors << result[:error]
114
+ end
115
+ end
116
+ end
117
+
118
+ def needs_rebuild?(content_file)
119
+ # Always rebuild if clean build
120
+ return true if @options[:clean]
121
+
122
+ output_file = content_file.output_file
123
+
124
+ # Rebuild if output doesn't exist
125
+ return true unless File.exist?(output_file)
126
+
127
+ output_mtime = File.mtime(output_file)
128
+
129
+ # Check if content file is newer
130
+ return true if content_file.mtime > output_mtime
131
+
132
+ # Check if template is newer
133
+ template_file = find_template_file(content_file.type)
134
+ return true if template_file && File.mtime(template_file) > output_mtime
135
+
136
+ # Check if layout is newer
137
+ layout_file = File.join(project.templates_dir, 'layout.html.erb')
138
+ return true if File.exist?(layout_file) && File.mtime(layout_file) > output_mtime
139
+
140
+ false
141
+ end
142
+
143
+ def needs_asset_copy?(asset_file)
144
+ # Always copy if clean build
145
+ return true if @options[:clean]
146
+
147
+ output_file = asset_file.output_file
148
+
149
+ # Copy if output doesn't exist
150
+ return true unless File.exist?(output_file)
151
+
152
+ # Copy if source is newer
153
+ asset_file.mtime > File.mtime(output_file)
154
+ end
155
+
156
+ def find_template_file(type)
157
+ template_path = File.join(project.templates_dir, "#{type}.html.erb")
158
+ File.exist?(template_path) ? template_path : nil
159
+ end
160
+
161
+ def generate_feeds_and_sitemap
162
+ # Generate RSS/Atom feeds only if there are blog posts
163
+ if has_blog_posts?
164
+ feed_generator = FeedGenerator.new(project)
165
+ feed_generator.generate_rss
166
+ feed_generator.generate_atom
167
+ end
168
+
169
+ # Always generate sitemap
170
+ sitemap_generator = SitemapGenerator.new(project)
171
+ sitemap_generator.generate
172
+ rescue StandardError => e
173
+ puts "⚠️ Warning: Failed to generate feeds/sitemap: #{e.message}" if @options[:verbose]
174
+ end
175
+
176
+ def has_blog_posts?
177
+ scanner.content_files.any? { |f| %w[blog post article news].include?(f.type) }
178
+ end
179
+ end
180
+
181
+ # Tracks build statistics
182
+ class BuildStats
183
+ attr_accessor :files_built, :files_skipped, :assets_copied, :assets_skipped, :errors, :total_time
184
+
185
+ def initialize
186
+ @files_built = 0
187
+ @files_skipped = 0
188
+ @assets_copied = 0
189
+ @assets_skipped = 0
190
+ @errors = []
191
+ @total_time = 0
192
+ end
193
+
194
+ def total_files
195
+ files_built + files_skipped
196
+ end
197
+
198
+ def total_assets
199
+ assets_copied + assets_skipped
200
+ end
201
+
202
+ def success?
203
+ errors.empty?
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jackdaw
4
+ # Main CLI entry point
5
+ class CLI < Thor
6
+ include CLIHelpers
7
+
8
+ def self.exit_on_failure?
9
+ true
10
+ end
11
+
12
+ desc 'build', 'Build the static site'
13
+ method_option :clean, type: :boolean, aliases: '-c', desc: 'Clean output directory before building'
14
+ method_option :watch, type: :boolean, aliases: '-w', desc: 'Watch for changes and rebuild'
15
+ method_option :verbose, type: :boolean, aliases: '-v', desc: 'Verbose output'
16
+ def build
17
+ Commands::Build.new(Project.new, options).execute
18
+ end
19
+
20
+ desc 'serve', 'Start development server'
21
+ method_option :port, type: :numeric, aliases: '-p', default: 4000, desc: 'Port to run server on'
22
+ method_option :host, type: :string, aliases: '-h', default: 'localhost', desc: 'Host to bind to'
23
+ method_option :livereload, type: :boolean, aliases: '-l', default: true, desc: 'Enable live reload'
24
+ def serve
25
+ Commands::Serve.new(Project.new, options).execute
26
+ end
27
+
28
+ desc 'create TEMPLATE NAME', 'Create a new content file from template'
29
+ method_option :dated, type: :boolean, desc: 'Add date prefix to filename'
30
+ method_option :no_date, type: :boolean, desc: 'Skip date prefix (overrides default behavior)'
31
+ def create(template, name)
32
+ Commands::Create.new(Project.new, template, name, options).execute
33
+ end
34
+
35
+ desc 'new NAME', 'Create a new site project'
36
+ def new(name)
37
+ Commands::New.new(name).execute
38
+ end
39
+
40
+ desc 'template SUBCOMMAND', 'Manage templates'
41
+ subcommand 'template', Commands::Template
42
+
43
+ desc 'version', 'Show version'
44
+ def version
45
+ puts "\n#{colorize('Jackdaw', :bold)} #{colorize("v#{Jackdaw::VERSION}", :cyan)}"
46
+ puts colorize('Lightning-fast static site generator', :magenta)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jackdaw
4
+ # Shared helper methods for CLI output formatting
5
+ module CLIHelpers
6
+ # ANSI color codes for beautiful output
7
+ COLORS = {
8
+ reset: "\e[0m",
9
+ bold: "\e[1m",
10
+ green: "\e[32m",
11
+ cyan: "\e[36m",
12
+ yellow: "\e[33m",
13
+ magenta: "\e[35m",
14
+ blue: "\e[34m"
15
+ }.freeze
16
+
17
+ def colorize(text, color)
18
+ "#{COLORS[color]}#{text}#{COLORS[:reset]}"
19
+ end
20
+
21
+ def success(text)
22
+ puts "#{colorize('✓', :green)} #{text}"
23
+ end
24
+
25
+ def info(text)
26
+ puts "#{colorize('→', :cyan)} #{text}"
27
+ end
28
+
29
+ def header(text)
30
+ puts "\n#{COLORS[:bold]}#{COLORS[:magenta]}#{text}#{COLORS[:reset]}"
31
+ end
32
+ end
33
+ end