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.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +5 -0
  3. data/exe/thunder +11 -0
  4. data/lib/thunder/application.rb +35 -0
  5. data/lib/thunder/cli/command.rb +19 -0
  6. data/lib/thunder/cli/commands/console.rb +24 -0
  7. data/lib/thunder/cli/commands/db/create.rb +20 -0
  8. data/lib/thunder/cli/commands/db/drop.rb +20 -0
  9. data/lib/thunder/cli/commands/db/migrate.rb +20 -0
  10. data/lib/thunder/cli/commands/db/reset.rb +22 -0
  11. data/lib/thunder/cli/commands/db/setup.rb +21 -0
  12. data/lib/thunder/cli/commands/generate/chunk.rb +23 -0
  13. data/lib/thunder/cli/commands/generate/command.rb +64 -0
  14. data/lib/thunder/cli/commands/generate/event.rb +28 -0
  15. data/lib/thunder/cli/commands/generate/model.rb +39 -0
  16. data/lib/thunder/cli/commands/generate/operation.rb +39 -0
  17. data/lib/thunder/cli/commands/generate/outbox.rb +33 -0
  18. data/lib/thunder/cli/commands/generate/subscriber.rb +33 -0
  19. data/lib/thunder/cli/commands/messaging/consumer.rb +52 -0
  20. data/lib/thunder/cli/commands/messaging/outbox.rb +36 -0
  21. data/lib/thunder/cli/commands/new.rb +44 -0
  22. data/lib/thunder/cli/commands/serve.rb +38 -0
  23. data/lib/thunder/cli/commands/spec.rb +77 -0
  24. data/lib/thunder/cli/commands/version.rb +16 -0
  25. data/lib/thunder/cli.rb +78 -0
  26. data/lib/thunder/console/backtrace.rb +36 -0
  27. data/lib/thunder/console/banner.rb +93 -0
  28. data/lib/thunder/console/completion.rb +68 -0
  29. data/lib/thunder/console/constants.rb +57 -0
  30. data/lib/thunder/console/evaluation.rb +72 -0
  31. data/lib/thunder/console/help.rb +30 -0
  32. data/lib/thunder/console/helpers.rb +16 -0
  33. data/lib/thunder/console/inspector.rb +114 -0
  34. data/lib/thunder/console/pretty.rb +70 -0
  35. data/lib/thunder/console/prompt.rb +52 -0
  36. data/lib/thunder/console/theme.rb +85 -0
  37. data/lib/thunder/console/views/collection.rb +172 -0
  38. data/lib/thunder/console/views/format.rb +114 -0
  39. data/lib/thunder/console/views/layout.rb +72 -0
  40. data/lib/thunder/console/views/model.rb +92 -0
  41. data/lib/thunder/console/views/operation.rb +112 -0
  42. data/lib/thunder/console/views/record.rb +95 -0
  43. data/lib/thunder/console/views.rb +45 -0
  44. data/lib/thunder/console.rb +167 -0
  45. data/lib/thunder/generator.rb +73 -0
  46. data/lib/thunder/templates/chunk/app/common/models/application_model.rb.erb +4 -0
  47. data/lib/thunder/templates/chunk/app/common/operations/external_operation.rb.erb +3 -0
  48. data/lib/thunder/templates/chunk/app/common/operations/internal_operation.rb.erb +3 -0
  49. data/lib/thunder/templates/chunk/app/external/operations/_keep.erb +0 -0
  50. data/lib/thunder/templates/chunk/app/internal/models/_keep.erb +0 -0
  51. data/lib/thunder/templates/chunk/app/internal/operations/_keep.erb +0 -0
  52. data/lib/thunder/templates/chunk/config/chunk.rb.erb +9 -0
  53. data/lib/thunder/templates/chunk/database/migrate/_keep.erb +0 -0
  54. data/lib/thunder/templates/event/event.rb.erb +6 -0
  55. data/lib/thunder/templates/model/migration.rb.erb +17 -0
  56. data/lib/thunder/templates/model/model.rb.erb +6 -0
  57. data/lib/thunder/templates/new/Gemfile.erb +21 -0
  58. data/lib/thunder/templates/new/README.md.erb +8 -0
  59. data/lib/thunder/templates/new/_gitignore.erb +4 -0
  60. data/lib/thunder/templates/new/_ruby-version.erb +2 -0
  61. data/lib/thunder/templates/new/bin/thunder.erb +6 -0
  62. data/lib/thunder/templates/new/chunks/_keep.erb +0 -0
  63. data/lib/thunder/templates/operation/external.rb.erb +24 -0
  64. data/lib/thunder/templates/operation/internal.rb.erb +26 -0
  65. data/lib/thunder/templates/outbox/migration.rb.erb +21 -0
  66. data/lib/thunder/templates/subscriber/subscriber.rb.erb +18 -0
  67. data/lib/thunder/version.rb +5 -0
  68. data/lib/thunder.rb +32 -0
  69. 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
@@ -0,0 +1,4 @@
1
+ <%# WARN: AI MADE -%>
2
+ class ApplicationModel < ThunderChunk::Model
3
+ self.abstract_class = true
4
+ end
@@ -0,0 +1,3 @@
1
+ <%# WARN: AI MADE -%>
2
+ class ExternalOperation < ThunderChunk::Operations::External
3
+ end
@@ -0,0 +1,3 @@
1
+ <%# WARN: AI MADE -%>
2
+ class InternalOperation < ThunderChunk::Operations::Internal
3
+ end
@@ -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
@@ -0,0 +1,6 @@
1
+ <%# WARN: AI MADE -%>
2
+ class <%= class_name %> < ThunderChunk::Event
3
+ <% fields.each do |name, type, optional| -%>
4
+ field :<%= name %>, <%= type %><%= ', optional: true' if optional %>
5
+ <% end -%>
6
+ end
@@ -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,6 @@
1
+ <%# WARN: AI MADE -%>
2
+ class <%= class_name %> < <%= parent %>
3
+ <% references.each do |name| -%>
4
+ belongs_to :<%= name %>
5
+ <% end -%>
6
+ 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
@@ -0,0 +1,8 @@
1
+ <%# WARN: AI MADE -%>
2
+ # <%= app_name %>
3
+
4
+ ```sh
5
+ thunder generate chunk NAME # a chunk in chunks/NAME
6
+ thunder db setup # creates and migrates the database of every chunk
7
+ thunder console -c NAME
8
+ ```
@@ -0,0 +1,4 @@
1
+ <%# WARN: AI MADE -%>
2
+ /log/
3
+ /tmp/
4
+ .env
@@ -0,0 +1,2 @@
1
+ <%# WARN: AI MADE -%>
2
+ <%= ruby_version %>
@@ -0,0 +1,6 @@
1
+ <%# WARN: AI MADE -%>
2
+ #!/usr/bin/env ruby
3
+ # frozen_string_literal: true
4
+
5
+ require 'bundler/setup'
6
+ load Gem.bin_path('thunder', 'thunder')
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