From 71f81c16ed8089e4b239451a2348e491eb72f2ba Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 26 Aug 2026 22:20:00 +0200 Subject: [PATCH] feat(cli): add project and resource generator with executable --- bin/prism | 11 +++ lib/prism/cli.rb | 181 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100755 bin/prism create mode 100644 lib/prism/cli.rb diff --git a/bin/prism b/bin/prism new file mode 100755 index 0000000..6b94b08 --- /dev/null +++ b/bin/prism @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby + +$LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) +require 'prism' + +begin + Prism::Cli.new.run(ARGV) +rescue Prism::Cli::UsageError => e + warn "error: #{e.message}" + exit 1 +end diff --git a/lib/prism/cli.rb b/lib/prism/cli.rb new file mode 100644 index 0000000..42f2f0e --- /dev/null +++ b/lib/prism/cli.rb @@ -0,0 +1,181 @@ +require 'fileutils' +require 'time' + +module Prism + class Cli + class UsageError < StandardError; end + + ServerScript = <<~'RUBY' + require_relative '../app' + + server = Prism::Server.new(APP_CLASS, port: (ARGV[0] || 3000).to_i).start + puts "listening on #{server.url}" + trap('INT') { server.stop; exit } + sleep + RUBY + + def initialize(root = Dir.pwd) + @root = root + end + + def run(argv) + command, *rest = argv + case command + when 'new' then new_app(rest.first || raise(UsageError, 'usage: prism new APP_NAME')) + when 'generate', 'g' then generate(rest) + else raise UsageError, "unknown command #{command.inspect}" + end + end + + def new_app(name) + app_dir = File.join(@root, name) + raise UsageError, "#{app_dir} already exists" if Dir.exist?(app_dir) + + %w[app/controllers app/models app/views/layouts db/migrate public bin].each do |subdir| + FileUtils.mkdir_p(File.join(app_dir, subdir)) + end + + write_file(File.join(app_dir, 'app', 'views', 'layouts', 'application.erb'), layout_template) + write_file(File.join(app_dir, 'app.rb'), app_boot_template(name)) + write_file(File.join(app_dir, 'config.ru'), config_ru_template(name)) + write_file(File.join(app_dir, 'bin', 'server'), ServerScript.sub('APP_CLASS', Inflector.camelize(name))) + File.chmod(0o755, File.join(app_dir, 'bin', 'server')) + app_dir + end + + def generate_migration(name, fields = []) + raise UsageError, 'usage: prism generate migration NAME [field:type ...]' unless name + version = next_migration_version + class_name = Inflector.camelize(name) + path = File.join(@root, 'db', 'migrate', "#{version}_#{Inflector.underscore(name)}.rb") + write_file(path, migration_template(class_name, name, fields)) + path + end + + def generate_model(name, fields = []) + raise UsageError, 'usage: prism generate model NAME [field:type ...]' unless name + class_name = Inflector.camelize(name) + model_path = File.join(@root, 'app', 'models', "#{Inflector.underscore(name)}.rb") + write_file(model_path, model_template(class_name)) + migration_path = generate_migration("create_#{Inflector.tableize(class_name)}", fields) + [model_path, migration_path] + end + + private + + def generate(rest) + kind, name, *fields = rest + case kind + when 'migration' then generate_migration(name, fields) + when 'model' then generate_model(name, fields) + else raise UsageError, "unknown generator #{kind.inspect}" + end + end + + def next_migration_version + version = Time.now.utc.strftime('%Y%m%d%H%M%S') + version = (version.to_i + 1).to_s while File.exist?(File.join(@root, 'db', 'migrate')) && + Dir.glob(File.join(@root, 'db', 'migrate', "#{version}_*.rb")).any? + version + end + + def write_file(path, content) + FileUtils.mkdir_p(File.dirname(path)) + File.write(path, content) + end + + def layout_template + <<~ERB + + + <%= defined?(title) ? title : 'Prism App' %> + <%== content %> + + ERB + end + + def app_boot_template(name) + class_name = Inflector.camelize(name) + <<~RUBY + require 'prism' + + APP_ROOT = __dir__ + + Prism::Orm.connect(File.join(APP_ROOT, 'db', 'development.pstore')) + Prism::Orm.migrator(File.join(APP_ROOT, 'db', 'migrate')).migrate! + + Prism::Controller.views_root(File.join(APP_ROOT, 'app', 'views')) + Prism::Controller.layout(:application) + + Dir[File.join(APP_ROOT, 'app', 'models', '*.rb')].sort.each { |file| require file } + Dir[File.join(APP_ROOT, 'app', 'controllers', '*.rb')].sort.each { |file| require file } + + class #{class_name} < Prism::Application + set :views, File.join(APP_ROOT, 'app', 'views') + use Prism::Middleware::Logger + use Prism::Middleware::ParamsParser + use Prism::Middleware::Static, root: File.join(APP_ROOT, 'public') + end + RUBY + end + + def config_ru_template(name) + <<~RUBY + require_relative 'app' + + run #{Inflector.camelize(name)} + RUBY + end + + def migration_template(class_name, name, fields) + match = name.match(/\Acreate_(.+)\z/i) + return plain_migration_template(class_name) unless match + + table = match[1] + columns = indent(fields.map { |field| field_line(field) }.join("\n"), 6) + <<~RUBY + class #{class_name} < Prism::Orm::Migration + def up + create_table :#{table} do |t| + #{columns} + t.timestamps + end + end + + def down + drop_table :#{table} + end + end + RUBY + end + + def plain_migration_template(class_name) + <<~RUBY + class #{class_name} < Prism::Orm::Migration + def up + end + + def down + end + end + RUBY + end + + def field_line(field) + column, type = field.split(':', 2) + "t.#{type || 'string'} :#{column}" + end + + def indent(text, spaces) + return '' if text.empty? + text.lines.map { |line| (' ' * spaces) + line }.join + end + + def model_template(class_name) + <<~RUBY + class #{class_name} < Prism::Orm::Model + end + RUBY + end + end +end