thundercli 0.2.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.
- checksums.yaml +7 -0
- data/README.md +5 -0
- data/exe/thunder +11 -0
- data/lib/thunder/application.rb +35 -0
- data/lib/thunder/cli/command.rb +19 -0
- data/lib/thunder/cli/commands/console.rb +24 -0
- data/lib/thunder/cli/commands/db/create.rb +20 -0
- data/lib/thunder/cli/commands/db/drop.rb +20 -0
- data/lib/thunder/cli/commands/db/migrate.rb +20 -0
- data/lib/thunder/cli/commands/db/reset.rb +22 -0
- data/lib/thunder/cli/commands/db/setup.rb +21 -0
- data/lib/thunder/cli/commands/generate/chunk.rb +23 -0
- data/lib/thunder/cli/commands/generate/command.rb +64 -0
- data/lib/thunder/cli/commands/generate/event.rb +28 -0
- data/lib/thunder/cli/commands/generate/model.rb +39 -0
- data/lib/thunder/cli/commands/generate/operation.rb +39 -0
- data/lib/thunder/cli/commands/generate/outbox.rb +33 -0
- data/lib/thunder/cli/commands/generate/subscriber.rb +33 -0
- data/lib/thunder/cli/commands/messaging/consumer.rb +52 -0
- data/lib/thunder/cli/commands/messaging/outbox.rb +36 -0
- data/lib/thunder/cli/commands/new.rb +44 -0
- data/lib/thunder/cli/commands/serve.rb +38 -0
- data/lib/thunder/cli/commands/spec.rb +77 -0
- data/lib/thunder/cli/commands/version.rb +16 -0
- data/lib/thunder/cli.rb +78 -0
- data/lib/thunder/console/backtrace.rb +36 -0
- data/lib/thunder/console/banner.rb +93 -0
- data/lib/thunder/console/completion.rb +68 -0
- data/lib/thunder/console/constants.rb +57 -0
- data/lib/thunder/console/evaluation.rb +72 -0
- data/lib/thunder/console/help.rb +30 -0
- data/lib/thunder/console/helpers.rb +16 -0
- data/lib/thunder/console/inspector.rb +114 -0
- data/lib/thunder/console/pretty.rb +70 -0
- data/lib/thunder/console/prompt.rb +52 -0
- data/lib/thunder/console/theme.rb +85 -0
- data/lib/thunder/console/views/collection.rb +172 -0
- data/lib/thunder/console/views/format.rb +114 -0
- data/lib/thunder/console/views/layout.rb +72 -0
- data/lib/thunder/console/views/model.rb +92 -0
- data/lib/thunder/console/views/operation.rb +112 -0
- data/lib/thunder/console/views/record.rb +95 -0
- data/lib/thunder/console/views.rb +45 -0
- data/lib/thunder/console.rb +167 -0
- data/lib/thunder/generator.rb +73 -0
- data/lib/thunder/templates/chunk/app/common/models/application_model.rb.erb +4 -0
- data/lib/thunder/templates/chunk/app/common/operations/external_operation.rb.erb +3 -0
- data/lib/thunder/templates/chunk/app/common/operations/internal_operation.rb.erb +3 -0
- data/lib/thunder/templates/chunk/app/external/operations/_keep.erb +0 -0
- data/lib/thunder/templates/chunk/app/internal/models/_keep.erb +0 -0
- data/lib/thunder/templates/chunk/app/internal/operations/_keep.erb +0 -0
- data/lib/thunder/templates/chunk/config/chunk.rb.erb +9 -0
- data/lib/thunder/templates/chunk/database/migrate/_keep.erb +0 -0
- data/lib/thunder/templates/event/event.rb.erb +6 -0
- data/lib/thunder/templates/model/migration.rb.erb +17 -0
- data/lib/thunder/templates/model/model.rb.erb +6 -0
- data/lib/thunder/templates/new/Gemfile.erb +21 -0
- data/lib/thunder/templates/new/README.md.erb +8 -0
- data/lib/thunder/templates/new/_gitignore.erb +4 -0
- data/lib/thunder/templates/new/_ruby-version.erb +2 -0
- data/lib/thunder/templates/new/bin/thunder.erb +6 -0
- data/lib/thunder/templates/new/chunks/_keep.erb +0 -0
- data/lib/thunder/templates/operation/external.rb.erb +24 -0
- data/lib/thunder/templates/operation/internal.rb.erb +26 -0
- data/lib/thunder/templates/outbox/migration.rb.erb +21 -0
- data/lib/thunder/templates/subscriber/subscriber.rb.erb +18 -0
- data/lib/thunder/version.rb +5 -0
- data/lib/thunder.rb +32 -0
- metadata +147 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# WARN: AI MADE
|
|
4
|
+
module Thunder
|
|
5
|
+
class Console
|
|
6
|
+
module Views
|
|
7
|
+
# An operation class as its interface, one section per part, one item per line: the params with their type
|
|
8
|
+
# (and optional when they are), what it uses, and the flow with the steps numbered in the order they run. A
|
|
9
|
+
# clause (transaction...) indents its steps; the step that ends the flow (success, render) is written as in the
|
|
10
|
+
# flow block, under the step names.
|
|
11
|
+
# => OPERATION books.add Library::Internal::Operations::Books::Add
|
|
12
|
+
# params 1
|
|
13
|
+
# title String
|
|
14
|
+
# uses
|
|
15
|
+
# model book
|
|
16
|
+
# flow
|
|
17
|
+
# 1 create_book
|
|
18
|
+
# success :book
|
|
19
|
+
class Operation
|
|
20
|
+
include Format
|
|
21
|
+
|
|
22
|
+
MAX_PARAMS = 16
|
|
23
|
+
MAX_STEPS = 24
|
|
24
|
+
KIND_LABELS = { external_operation: 'external' }.freeze
|
|
25
|
+
STEP_INDENT = ' '
|
|
26
|
+
|
|
27
|
+
def initialize(klass)
|
|
28
|
+
@klass = klass
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call
|
|
32
|
+
header = "#{badge('OPERATION')} #{paint(key_for(@klass), :bold)} #{paint(@klass.name, :faint)}"
|
|
33
|
+
sections = [
|
|
34
|
+
(section('params', params, count: parameters.size) if parameters.any?),
|
|
35
|
+
(section('uses', uses) if dependencies.any?),
|
|
36
|
+
section('flow', @klass.flow.steps.any? ? flow : [paint('empty', :faint)])
|
|
37
|
+
]
|
|
38
|
+
[header, *sections.compact.flatten].join("\n")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def parameters = @klass.respond_to?(:base_parameter) ? @klass.base_parameter.configuration : {}
|
|
44
|
+
|
|
45
|
+
def dependencies = @klass.respond_to?(:dependencies) ? @klass.dependencies : []
|
|
46
|
+
|
|
47
|
+
def params
|
|
48
|
+
lines = aligned(parameters.map do |name, parameter|
|
|
49
|
+
[name.to_s, paint(type(parameter), :faint), *(paint('optional', :faint) if parameter.optional?),
|
|
50
|
+
*(paint('sensitive', :warn) if parameter.sensitive?)]
|
|
51
|
+
end)
|
|
52
|
+
cap(lines, MAX_PARAMS, 'params')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def type(parameter)
|
|
56
|
+
case parameter
|
|
57
|
+
when ThunderOperation::Parameters::HashParameter then hash_type(parameter.configuration.keys)
|
|
58
|
+
when ThunderOperation::Parameters::ArrayParameter
|
|
59
|
+
parameter.items ? "[#{type(parameter.items)}]" : 'Array'
|
|
60
|
+
else
|
|
61
|
+
parameter.type&.name&.demodulize || 'any'
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The first keys of a hash param: {street, city, zip, …}.
|
|
66
|
+
def hash_type(keys) = keys.empty? ? 'Hash' : "{#{keys.first(3).join(', ')}#{', …' if keys.size > 3}}"
|
|
67
|
+
|
|
68
|
+
def uses
|
|
69
|
+
aligned(dependencies.map do |dependency|
|
|
70
|
+
[paint(KIND_LABELS.fetch(dependency.kind, dependency.kind).to_s, :faint), dependency.path]
|
|
71
|
+
end)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def flow
|
|
75
|
+
@number = 0
|
|
76
|
+
@hidden = 0
|
|
77
|
+
lines = flow_lines(@klass.flow.steps, 0)
|
|
78
|
+
@more << paint("… #{@hidden} more steps", :faint) if @hidden.positive?
|
|
79
|
+
lines
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Past MAX_STEPS the numbered steps are counted, not listed; "… N more" takes the place of the first one left
|
|
83
|
+
# out. The steps that end the flow are always listed.
|
|
84
|
+
def flow_lines(steps, depth)
|
|
85
|
+
indent = STEP_INDENT * depth
|
|
86
|
+
steps.flat_map do |step|
|
|
87
|
+
case step
|
|
88
|
+
when ThunderOperation::Steps::Clause
|
|
89
|
+
heading = paint(step.send(:signature, step.name, step.args, step.kwargs), :bold)
|
|
90
|
+
["#{indent}#{heading}", *flow_lines(step.steps, depth + 1)]
|
|
91
|
+
when ThunderOperation::Steps::Method
|
|
92
|
+
@number += 1
|
|
93
|
+
next ["#{indent}#{paint(@number.to_s.rjust(2), :faint)} #{step.name}"] if @number <= MAX_STEPS
|
|
94
|
+
|
|
95
|
+
@hidden += 1
|
|
96
|
+
# Filled in by flow once the hidden steps are counted.
|
|
97
|
+
@hidden == 1 ? [@more = "#{indent} "] : []
|
|
98
|
+
else
|
|
99
|
+
["#{indent} #{paint(step.to_s.sub(/\((.*)\)\z/, ' \1'), :ok)}"]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def cap(lines, max, name)
|
|
105
|
+
return lines if lines.size <= max
|
|
106
|
+
|
|
107
|
+
[*lines.first(max - 1), paint("… #{lines.size - (max - 1)} more #{name}", :faint)]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# WARN: AI MADE
|
|
4
|
+
module Thunder
|
|
5
|
+
class Console
|
|
6
|
+
module Views
|
|
7
|
+
# A record as a block: its key and primary key in the header (a uuid in full), the state and unsaved changes,
|
|
8
|
+
# then one row per filled attribute in schema order, a belongs_to key under the association's name, the nil
|
|
9
|
+
# attributes on one row and the timestamps on another.
|
|
10
|
+
# => order 7c1e0b5a-3f2a-4d9e-8b1c-2a9f0e6d4c11 persisted · changed: status
|
|
11
|
+
# │ number ORD-2026-000431
|
|
12
|
+
# │ status "paid" (was "pending")
|
|
13
|
+
# │ customer 9d0f3a2b… customer_id
|
|
14
|
+
# │ nil coupon_id, canceled_at, +6
|
|
15
|
+
class Record
|
|
16
|
+
include Format
|
|
17
|
+
|
|
18
|
+
MAX_ROWS = 20
|
|
19
|
+
|
|
20
|
+
def initialize(record)
|
|
21
|
+
@record = record
|
|
22
|
+
@klass = record.class
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call
|
|
26
|
+
[header, *rows(attribute_rows, indent: 3, guide: :accent)].join("\n")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def header
|
|
32
|
+
id = @record[@klass.primary_key] if @klass.primary_key
|
|
33
|
+
head = "#{badge(key_for(@klass))} "
|
|
34
|
+
head += "#{paint(uuid?(id) ? id : "##{id}", :bold)} " if id
|
|
35
|
+
head + paint(state(screen_width - 3 - visible_size(head)), :faint)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The changed attributes, or only how many when they do not fit the width left on the header line.
|
|
39
|
+
def state(width)
|
|
40
|
+
base = if @record.new_record? then 'new'
|
|
41
|
+
elsif @record.destroyed? then 'destroyed'
|
|
42
|
+
else 'persisted'
|
|
43
|
+
end
|
|
44
|
+
changed = @record.changes_to_save.keys
|
|
45
|
+
return base if changed.empty?
|
|
46
|
+
|
|
47
|
+
full = "#{base} · changed: #{changed.join(', ')}"
|
|
48
|
+
full.size <= width ? full : "#{base} · #{changed.size} changed"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def attribute_rows
|
|
52
|
+
names = @record.attribute_names - [@klass.primary_key]
|
|
53
|
+
stamps = names & TIMESTAMPS
|
|
54
|
+
nils, filled = (names - stamps).partition { |name| @record[name].nil? }
|
|
55
|
+
labels = filled.map { |name| label(name) }
|
|
56
|
+
width = screen_width - MARGIN - [*labels, 'timestamps'].map(&:size).max - 2
|
|
57
|
+
|
|
58
|
+
rows = filled.zip(labels).map { |name, label| [label, attribute(name, width)] }
|
|
59
|
+
[
|
|
60
|
+
*cap(rows, MAX_ROWS, ' · .attributes for all'),
|
|
61
|
+
(['nil', list(nils, width)] if nils.any?),
|
|
62
|
+
(['timestamps', timestamps(stamps, width)] if stamps.any?)
|
|
63
|
+
]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def label(name) = foreign_keys.fetch(name, name)
|
|
67
|
+
|
|
68
|
+
def foreign_keys
|
|
69
|
+
@foreign_keys ||= @klass.reflect_on_all_associations(:belongs_to).to_h do |association|
|
|
70
|
+
[association.foreign_key.to_s, association.name.to_s]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def attribute(name, width)
|
|
75
|
+
value = @record[name]
|
|
76
|
+
change = @record.changes_to_save[name]
|
|
77
|
+
was = change ? " (was #{truncate(change.first.inspect, 30)})" : ''
|
|
78
|
+
text = if foreign_keys.key?(name) then "#{paint(short_id(value), :bold)} #{paint(name, :faint)}"
|
|
79
|
+
else value_text(value, width - was.size)
|
|
80
|
+
end
|
|
81
|
+
"#{text}#{paint(was, :faint)}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def timestamps(stamps, width)
|
|
85
|
+
parts = stamps.map do |name|
|
|
86
|
+
value = @record[name]
|
|
87
|
+
"#{paint(name.delete_suffix('_at'), :faint)} #{value ? value_text(value, 20) : paint('nil', :faint)}"
|
|
88
|
+
end
|
|
89
|
+
line = parts.join(paint(' · ', :faint))
|
|
90
|
+
visible_size(line) <= width ? line : parts.join("\n")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'views/layout'
|
|
4
|
+
require_relative 'views/format'
|
|
5
|
+
require_relative 'views/record'
|
|
6
|
+
require_relative 'views/collection'
|
|
7
|
+
require_relative 'views/model'
|
|
8
|
+
require_relative 'views/operation'
|
|
9
|
+
|
|
10
|
+
# WARN: AI MADE
|
|
11
|
+
module Thunder
|
|
12
|
+
class Console
|
|
13
|
+
# How the console prints the framework's own objects: records, relations and arrays of records, model classes and
|
|
14
|
+
# operation classes. Anything else goes to Pretty.
|
|
15
|
+
module Views
|
|
16
|
+
extend Format
|
|
17
|
+
|
|
18
|
+
# The view for the value; nil when it has none.
|
|
19
|
+
def self.for(value)
|
|
20
|
+
case value
|
|
21
|
+
when ActiveRecord::Base then Record.new(value)
|
|
22
|
+
when ActiveRecord::Relation then Collection.new(value)
|
|
23
|
+
when Array then Collection.new(value) if Collection.records?(value)
|
|
24
|
+
when Class then class_view(value)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.class_view(klass)
|
|
29
|
+
if klass < ActiveRecord::Base && !klass.abstract_class? then Model.new(klass)
|
|
30
|
+
elsif klass < ThunderOperation::Base then Operation.new(klass)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# A record inside a hash or an array: its key and id ("book #1", "order 7c1e0b5a…").
|
|
35
|
+
def self.reference(record)
|
|
36
|
+
id = record[record.class.primary_key] if record.class.primary_key
|
|
37
|
+
id = if id.nil? then 'new'
|
|
38
|
+
elsif uuid?(id) then short_id(id)
|
|
39
|
+
else "##{id}"
|
|
40
|
+
end
|
|
41
|
+
paint("#{key_for(record.class)} #{id}", :accent)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'irb'
|
|
4
|
+
|
|
5
|
+
require_relative 'console/theme'
|
|
6
|
+
require_relative 'console/prompt'
|
|
7
|
+
require_relative 'console/banner'
|
|
8
|
+
require_relative 'console/evaluation'
|
|
9
|
+
require_relative 'console/pretty'
|
|
10
|
+
require_relative 'console/views'
|
|
11
|
+
require_relative 'console/inspector'
|
|
12
|
+
require_relative 'console/completion'
|
|
13
|
+
require_relative 'console/constants'
|
|
14
|
+
require_relative 'console/backtrace'
|
|
15
|
+
require_relative 'console/help'
|
|
16
|
+
require_relative 'console/helpers'
|
|
17
|
+
|
|
18
|
+
# WARN: AI MADE
|
|
19
|
+
module Thunder
|
|
20
|
+
# IRB with one chunk of the application booted. `chunk :name` restarts the process on another chunk, so a console
|
|
21
|
+
# never holds two chunks: no constants, connection or records of the previous one, and each chunk keeps its own
|
|
22
|
+
# bundle.
|
|
23
|
+
class Console
|
|
24
|
+
include Theme
|
|
25
|
+
|
|
26
|
+
# Commands kept from IRB; the rest (ls, cd, show_source, edit, debug...) are removed.
|
|
27
|
+
IRB_COMMANDS = %i[exit exit!].freeze
|
|
28
|
+
|
|
29
|
+
IRB::Inspector.def_inspector(:thunder) do |value, colorize: true|
|
|
30
|
+
Console.current.inspector.call(value, colorize:)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
attr_accessor :current
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_reader :application, :chunk, :env, :evaluation, :inspector
|
|
38
|
+
|
|
39
|
+
def initialize(application, chunk_name, env:)
|
|
40
|
+
@application = application
|
|
41
|
+
@env = env
|
|
42
|
+
@chunk = chunk_name && application.chunk(chunk_name)
|
|
43
|
+
@used = []
|
|
44
|
+
@constants = Constants.new
|
|
45
|
+
@prompt = Prompt.new(self)
|
|
46
|
+
@evaluation = Evaluation.new(@prompt)
|
|
47
|
+
@inspector = Inspector.new(self)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def start
|
|
51
|
+
self.class.current = self
|
|
52
|
+
ENV['THUNDER_ENV'] = env
|
|
53
|
+
# The console gem (async) logs nothing here: 5 is above fatal. CONSOLE_LEVEL=info turns it on.
|
|
54
|
+
ENV['CONSOLE_LEVEL'] ||= '5'
|
|
55
|
+
puts Banner.new(self).lines(boot_error: chunk && boot)
|
|
56
|
+
|
|
57
|
+
TOPLEVEL_BINDING.receiver.extend(Helpers)
|
|
58
|
+
IRB.setup(nil, argv: [])
|
|
59
|
+
configure_irb
|
|
60
|
+
|
|
61
|
+
irb = IRB::Irb.new
|
|
62
|
+
install_completion(irb.context.io)
|
|
63
|
+
irb.run(IRB.conf)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def within_chunk
|
|
67
|
+
return yield(self) if chunk
|
|
68
|
+
|
|
69
|
+
puts paint("no chunk selected; chunks: #{chunks_list} (chunk :name)", :warn)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def use!(kind, key) = selected_chunk.registry.get!(kind, key)
|
|
73
|
+
|
|
74
|
+
# Defines the key's path constant on Object: 'book' -> Book, 'books.add' -> Books::Add. reload! repoints it.
|
|
75
|
+
# false when the key is not registered or its path hits a constant use did not define (see Constants); use! still
|
|
76
|
+
# returns the class.
|
|
77
|
+
def use(kind, key) # rubocop:disable Naming/PredicateMethod -- a console helper named like the DSL, returning true/false
|
|
78
|
+
klass = selected_chunk.registry.get(kind, key)
|
|
79
|
+
return false unless klass
|
|
80
|
+
|
|
81
|
+
if (taken = @constants.define(key, klass))
|
|
82
|
+
puts paint("#{taken} already exists; use! #{kind.inspect}, '#{key}' returns the class", :warn)
|
|
83
|
+
return false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
@used << [kind, key] unless @used.include?([kind, key])
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def reload!
|
|
91
|
+
@constants.clear
|
|
92
|
+
error = boot
|
|
93
|
+
return puts(paint("✗ boot failed: #{error.class}: #{error.message.lines.first.to_s.strip}", :fail)) if error
|
|
94
|
+
|
|
95
|
+
puts paint('reloaded', :ok)
|
|
96
|
+
@used.each { |kind, key| use(kind, key) }
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def toggle_sql_log
|
|
101
|
+
evaluation.sql_log = !evaluation.sql_log
|
|
102
|
+
puts paint("sql log #{evaluation.sql_log ? 'on' : 'off'}", :muted)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Replaces this process with a console on another chunk. An unknown name prints the chunks and keeps the current
|
|
106
|
+
# one.
|
|
107
|
+
def change_chunk(name)
|
|
108
|
+
application.chunk(name)
|
|
109
|
+
save_history
|
|
110
|
+
|
|
111
|
+
ruby_flags = defined?(DEBUGGER__) ? ['-rdebug'] : []
|
|
112
|
+
exec(RbConfig.ruby, *ruby_flags, $PROGRAM_NAME, 'console', '-c', name.to_s, '--env', env)
|
|
113
|
+
rescue Error => e
|
|
114
|
+
puts paint(e.message, :warn)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
# nil when the chunk booted, the error otherwise.
|
|
120
|
+
def boot
|
|
121
|
+
selected_chunk.boot
|
|
122
|
+
nil
|
|
123
|
+
rescue StandardError, ScriptError => e
|
|
124
|
+
e
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def selected_chunk
|
|
128
|
+
chunk || raise(Error, "no chunk selected; chunks: #{chunks_list} (chunk :name)")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def chunks_list = application.chunks.map(&:identifier).join(', ')
|
|
132
|
+
|
|
133
|
+
def configure_irb
|
|
134
|
+
restrict_commands
|
|
135
|
+
evaluation.install
|
|
136
|
+
IRB.conf[:INSPECT_MODE] = :thunder
|
|
137
|
+
IRB.conf[:NEWLINE_BEFORE_MULTILINE_OUTPUT] = false
|
|
138
|
+
IRB.conf[:PROMPT][:THUNDER] = @prompt.irb_prompts
|
|
139
|
+
IRB.conf[:PROMPT_MODE] = :THUNDER
|
|
140
|
+
IRB.conf[:BACKTRACE_FILTER] = Backtrace.new(application.root)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# IRB keeps commands in a global registry and caches which names they override; both are reset here, as IRB's own
|
|
144
|
+
# registration does. Internal API of irb 1.18.
|
|
145
|
+
def restrict_commands
|
|
146
|
+
IRB::Command.commands.select! { |name, _| IRB_COMMANDS.include?(name) }
|
|
147
|
+
IRB::Command.register(:help, Help)
|
|
148
|
+
IRB::Command.class_variable_set(:@@command_override_policies, nil) # rubocop:disable Style/ClassVars -- IRB's cache
|
|
149
|
+
IRB::HelperMethod.helper_methods.each_key do |name|
|
|
150
|
+
IRB::HelpersContainer.send(:remove_method, name) if IRB::HelpersContainer.method_defined?(name)
|
|
151
|
+
end
|
|
152
|
+
IRB::HelperMethod.helper_methods.clear
|
|
153
|
+
IRB.conf[:COMMAND_ALIASES] = {}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Only the Reline input method has a completor; a non-terminal stdin uses another one.
|
|
157
|
+
def install_completion(io)
|
|
158
|
+
completor = io.instance_variable_get(:@completor)
|
|
159
|
+
completor&.singleton_class&.prepend(Completion)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def save_history
|
|
163
|
+
io = IRB.CurrentContext&.io
|
|
164
|
+
io.save_history if io.respond_to?(:save_history)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# WARN: AI MADE
|
|
4
|
+
require 'erb'
|
|
5
|
+
require 'pathname'
|
|
6
|
+
|
|
7
|
+
module Thunder
|
|
8
|
+
# Writes files from the templates in lib/thunder/templates. A `.erb` file is rendered with the locals and loses the
|
|
9
|
+
# extension; a leading `_` in a name becomes a dot (`_gitignore` -> `.gitignore`), so the gem packages it.
|
|
10
|
+
# Nothing is written when a target exists, unless force is given.
|
|
11
|
+
class Generator
|
|
12
|
+
TEMPLATES = Pathname(__dir__).join('templates')
|
|
13
|
+
|
|
14
|
+
# The repository of the framework, for the Gemfile of a generated app while the gems are not published.
|
|
15
|
+
FRAMEWORK_ROOT = Pathname(__dir__).join('../../..').expand_path
|
|
16
|
+
|
|
17
|
+
def initialize(root, force: false, out: $stdout)
|
|
18
|
+
@root = Pathname(root)
|
|
19
|
+
@force = force
|
|
20
|
+
@out = out
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Renders one template into target, relative to the root. namespace wraps the result in those modules.
|
|
24
|
+
def template(source, target, namespace: [], **locals)
|
|
25
|
+
write({ @root.join(target) => [nest(namespace, render(TEMPLATES.join(source), locals)), nil] })
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Copies a template directory into target, relative to the root.
|
|
29
|
+
def directory(source, target, **locals)
|
|
30
|
+
source = TEMPLATES.join(source)
|
|
31
|
+
files = source.glob('**/*', File::FNM_DOTMATCH).select(&:file?).to_h do |file|
|
|
32
|
+
path = file.relative_path_from(source).to_s.gsub(%r{(\A|/)_}, '\1.').delete_suffix('.erb')
|
|
33
|
+
|
|
34
|
+
[@root.join(target, path), [render(file, locals), file.stat.mode]]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
write(files)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def render(file, locals)
|
|
43
|
+
file.extname == '.erb' ? ERB.new(file.read, trim_mode: '-').result_with_hash(locals) : file.read
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Each module opens one level more; the code goes inside the last one.
|
|
47
|
+
def nest(namespace, code)
|
|
48
|
+
return code if namespace.empty?
|
|
49
|
+
|
|
50
|
+
inner = nest(namespace.drop(1), code).gsub(/^(?=.)/, ' ')
|
|
51
|
+
|
|
52
|
+
"module #{namespace.first}\n#{inner}end\n"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def write(files)
|
|
56
|
+
existing = files.keys.select(&:exist?)
|
|
57
|
+
if existing.any? && !@force
|
|
58
|
+
raise Error, "#{existing.map { |path| relative(path) }.join(', ')} already exists (--force overwrites)"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
files.each do |path, (content, mode)|
|
|
62
|
+
path.dirname.mkpath
|
|
63
|
+
path.write(content)
|
|
64
|
+
path.chmod(mode) if mode
|
|
65
|
+
@out.puts "create #{relative(path)}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def relative(path)
|
|
70
|
+
path.expand_path.relative_path_from(Pathname.pwd).to_s
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<%# WARN: AI MADE -%>
|
|
2
|
+
ThunderChunk::Configuration.new do
|
|
3
|
+
# Database name: <%= chunk_name %>_<env> unless database.database is given.
|
|
4
|
+
database.adapter = 'postgresql'
|
|
5
|
+
database.host = ENV.fetch('POSTGRES_HOST', '127.0.0.1')
|
|
6
|
+
database.port = Integer(ENV.fetch('POSTGRES_PORT', 5432))
|
|
7
|
+
database.username = ENV.fetch('POSTGRES_USERNAME', 'postgres')
|
|
8
|
+
database.password = ENV.fetch('POSTGRES_PASSWORD', 'password')
|
|
9
|
+
end
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%# WARN: AI MADE -%>
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
class <%= migration_class %> < ActiveRecord::Migration[<%= migration_version %>]
|
|
5
|
+
def change
|
|
6
|
+
create_table :<%= table_name %>, id: :uuid do |t|
|
|
7
|
+
<% columns.each do |name, type| -%>
|
|
8
|
+
<% if type == 'references' -%>
|
|
9
|
+
t.references :<%= name %>, foreign_key: true, type: :uuid
|
|
10
|
+
<% else -%>
|
|
11
|
+
t.<%= type %> :<%= name %>
|
|
12
|
+
<% end -%>
|
|
13
|
+
<% end -%>
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<%# WARN: AI MADE -%>
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
source 'https://rubygems.org'
|
|
5
|
+
|
|
6
|
+
<% if framework_path -%>
|
|
7
|
+
# The gems of the framework from its repository, while they are not published.
|
|
8
|
+
<% %w[thunderfoundation thunderframe thunderoperation thunderchunk thunderwire thunder thunderspec].each do |gem| -%>
|
|
9
|
+
gem '<%= gem %>', path: '<%= framework_path %>/<%= gem %>'
|
|
10
|
+
<% end -%>
|
|
11
|
+
gem 'thunderbolt', path: '<%= framework_path %>'
|
|
12
|
+
<% else -%>
|
|
13
|
+
gem 'thunderbolt'
|
|
14
|
+
<% end -%>
|
|
15
|
+
|
|
16
|
+
gem 'pg'
|
|
17
|
+
|
|
18
|
+
# The chunks' specs, run by `thunder spec`.
|
|
19
|
+
group :test do
|
|
20
|
+
gem 'thunderspec', require: false
|
|
21
|
+
end
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<%# WARN: AI MADE -%>
|
|
2
|
+
class <%= class_name %> < <%= parent %>
|
|
3
|
+
<% params.each do |name, type, optional| -%>
|
|
4
|
+
param :<%= name %>, <%= type %><%= ', optional: true' if optional %>
|
|
5
|
+
<% end -%>
|
|
6
|
+
<% uses.each do |line| -%>
|
|
7
|
+
<%= line %>
|
|
8
|
+
<% end -%>
|
|
9
|
+
<% unless params.empty? && uses.empty? -%>
|
|
10
|
+
|
|
11
|
+
<% end -%>
|
|
12
|
+
flow do
|
|
13
|
+
success
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
steps do
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
returns Hash do
|
|
20
|
+
<% returns.each do |name, type| -%>
|
|
21
|
+
field :<%= name %>, <%= type %>
|
|
22
|
+
<% end -%>
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<%# WARN: AI MADE -%>
|
|
2
|
+
class <%= class_name %> < <%= parent %>
|
|
3
|
+
<% params.each do |name, type, optional| -%>
|
|
4
|
+
param :<%= name %>, <%= type %><%= ', optional: true' if optional %>
|
|
5
|
+
<% end -%>
|
|
6
|
+
<% uses.each do |line| -%>
|
|
7
|
+
<%= line %>
|
|
8
|
+
<% end -%>
|
|
9
|
+
<% unless params.empty? && uses.empty? -%>
|
|
10
|
+
|
|
11
|
+
<% end -%>
|
|
12
|
+
flow do
|
|
13
|
+
success
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
steps do
|
|
17
|
+
end
|
|
18
|
+
<% unless returns.empty? -%>
|
|
19
|
+
|
|
20
|
+
returns Hash do
|
|
21
|
+
<% returns.each do |name, type| -%>
|
|
22
|
+
field :<%= name %>, <%= type %>
|
|
23
|
+
<% end -%>
|
|
24
|
+
end
|
|
25
|
+
<% end -%>
|
|
26
|
+
end
|