rpremote 0.6.0 → 0.7.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.
@@ -9,78 +9,106 @@ module Rpremote
9
9
  SHELL_READY_TIMEOUT = 2.0
10
10
  RETRY_INTERVAL = 0.25
11
11
 
12
+ Context = Data.define(:options, :output, :error, :services, :target_options, :firmware_path)
13
+
12
14
  class Error < Rpremote::Error; end
13
15
 
14
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
15
16
  def self.run(args, defaults:, output: $stdout, error: $stderr, services: {})
16
17
  options = parse_options(args, defaults)
17
18
  raise ArgumentError, "usage: rpremote deploy PATH [options]" unless args.length == 1
18
19
 
19
- project = File.expand_path(args.first)
20
- library, remote_library, source = project_files(project)
20
+ library, remote_library, source = project_files(File.expand_path(args.first))
21
21
  Language.validate!(options[:language])
22
+ context = deployment_context(options, output, error, services)
23
+ build_firmware(context)
24
+ result = flash_firmware(context)
25
+ connect_to_shell(context, result)
26
+ push_library(context, result, library, remote_library)
27
+ run_source(context, result, source)
28
+ rescue Errno::ENOENT => e
29
+ raise ArgumentError, e.message
30
+ end
22
31
 
23
- flasher = services.fetch(:flasher, Flasher)
24
- serial = services.fetch(:serial, Serial)
25
- device = services.fetch(:device, Device)
26
- runner = services.fetch(:runner, Runner)
32
+ def self.deployment_context(options, output, error, services)
27
33
  target_options = options.slice(:language, :language_version, :board, :cache_dir, :firmware)
28
34
  firmware_path = Target.new(**target_options).firmware_path(root: Dir.pwd)
35
+ Context.new(options:, output:, error:, services:, target_options:, firmware_path:)
36
+ end
37
+ private_class_method :deployment_context
38
+
39
+ def self.build_firmware(context)
40
+ return unless context.options[:build]
41
+
42
+ builder = context.services.fetch(:builder) { Builder.new }
43
+ context.output.puts("deploy build: #{context.firmware_path}")
44
+ builder.build(
45
+ **context.target_options,
46
+ mrbgems_lock: context.options[:mrbgems_lock],
47
+ output: context.output,
48
+ error: context.error
49
+ )
50
+ end
51
+ private_class_method :build_firmware
29
52
 
30
- if options[:build]
31
- builder = services.fetch(:builder) { Builder.new }
32
- output.puts("deploy build: #{firmware_path}")
33
- builder.build(
34
- **target_options,
35
- mrbgems: options[:mrbgems],
36
- output: output,
37
- error: error
38
- )
39
- end
40
-
41
- flash_service = flasher.new(timeout: options[:timeout])
42
- bootsel_mount = resolve_bootsel_mount(options, flash_service, output, services)
43
-
53
+ def self.flash_firmware(context)
54
+ options = context.options
55
+ flash_service = context.services.fetch(:flasher, Flasher).new(timeout: options[:timeout])
56
+ bootsel_mount = resolve_bootsel_mount(options, flash_service, context.output, context.services)
44
57
  mount = options[:mount] || "an automatically detected RP2350 BOOTSEL volume"
45
- output.puts("deploy flash: #{firmware_path} to #{mount}; this replaces persistent board firmware")
46
- result = flash_service.flash(
47
- firmware_path,
48
- mount: bootsel_mount,
49
- port: options[:port]
58
+ context.output.puts(
59
+ "deploy flash: #{context.firmware_path} to #{mount}; this replaces persistent board firmware"
50
60
  )
61
+ flash_service.flash(context.firmware_path, mount: bootsel_mount, port: options[:port])
62
+ end
63
+ private_class_method :flash_firmware
51
64
 
52
- connection_options = options.except(:build).merge(port: result.port)
53
- output.puts("deploy connect: waiting for R2P2 Shell on #{result.port}")
65
+ def self.connect_to_shell(context, result)
66
+ options = context.options
67
+ context.output.puts("deploy connect: waiting for R2P2 Shell on #{result.port}")
54
68
  wait_for_shell(
55
- port: result.port, baud: options[:baud], timeout: options[:timeout], serial: serial,
56
- shell: services.fetch(:shell, Shell),
57
- sleeper: services.fetch(:sleeper) { ->(seconds) { sleep(seconds) } },
58
- clock: services.fetch(:clock) { -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) } }
69
+ port: result.port, baud: options[:baud], timeout: options[:timeout],
70
+ serial: context.services.fetch(:serial, Serial),
71
+ shell: context.services.fetch(:shell, Shell),
72
+ sleeper: context.services.fetch(:sleeper) { ->(seconds) { sleep(seconds) } },
73
+ clock: context.services.fetch(:clock) { -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) } }
59
74
  )
60
- output.puts("deploy connect: ready on #{result.port}")
75
+ context.output.puts("deploy connect: ready on #{result.port}")
76
+ end
77
+ private_class_method :connect_to_shell
61
78
 
62
- if File.directory?(library)
63
- output.puts("deploy push: #{library} -> :#{remote_library}")
64
- recursive_copy = services.fetch(:recursive_copy) do
65
- RecursiveCopy.new(output: output, serial: serial, device: device)
66
- end
67
- recursive_copy.call(library, ":#{remote_library}", connection_options)
68
- else
69
- output.puts("deploy push: skipped; directory not found: #{library}")
79
+ def self.push_library(context, result, library, remote_library)
80
+ unless File.directory?(library)
81
+ context.output.puts("deploy push: skipped; directory not found: #{library}")
82
+ return
70
83
  end
71
84
 
72
- output.puts("deploy run: #{source} on #{result.port}")
73
- source_data = prepend_mrbgem_requires(File.binread(source), options[:mrbgems])
85
+ context.output.puts("deploy push: #{library} -> :#{remote_library}")
86
+ recursive_copy = context.services.fetch(:recursive_copy) do
87
+ RecursiveCopy.new(
88
+ output: context.output,
89
+ serial: context.services.fetch(:serial, Serial),
90
+ device: context.services.fetch(:device, Device)
91
+ )
92
+ end
93
+ connection_options = context.options.except(:build).merge(port: result.port)
94
+ recursive_copy.call(library, ":#{remote_library}", connection_options)
95
+ end
96
+ private_class_method :push_library
97
+
98
+ def self.run_source(context, result, source)
99
+ options = context.options
100
+ context.output.puts("deploy run: #{source} on #{result.port}")
101
+ source_data = prepend_mrbgem_requires(File.binread(source), options[:mrbgems_lock])
102
+ serial = context.services.fetch(:serial, Serial)
103
+ runner = context.services.fetch(:runner, Runner)
74
104
  run_output = serial.open(result.port, baud: options[:baud]) do |port|
75
105
  runner.new(port, timeout: options[:timeout]).run(
76
- source_data, output: output, cleanup: false, remote_path: Runner::DEPLOY_REMOTE_PATH
106
+ source_data, output: context.output, cleanup: false, remote_path: Runner::DEPLOY_REMOTE_PATH
77
107
  )
78
108
  end
79
- output.puts("deploy run: completed; output bytes: #{run_output.to_s.bytesize}")
80
- rescue Errno::ENOENT => e
81
- raise ArgumentError, e.message
109
+ context.output.puts("deploy run: completed; output bytes: #{run_output.to_s.bytesize}")
82
110
  end
83
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
111
+ private_class_method :run_source
84
112
 
85
113
  def self.project_files(project)
86
114
  raise ArgumentError, "project directory not found: #{project}" unless File.directory?(project)
@@ -94,10 +122,13 @@ module Rpremote
94
122
  end
95
123
  private_class_method :project_files
96
124
 
97
- def self.prepend_mrbgem_requires(source, definition)
98
- return source if definition == false
125
+ def self.prepend_mrbgem_requires(source, lock_file)
126
+ return source if lock_file == false
127
+
128
+ path = lock_file || Mrbgems::DEFAULT_LOCK_PATH
129
+ raise ArgumentError, "mrbgems lock file does not exist: #{path}" if lock_file && !File.file?(path)
99
130
 
100
- Mrbgems.new(path: definition || Mrbgems::DEFAULT_PATH, cwd: Dir.pwd).prepend_requires(source)
131
+ Mrbgems.new(lock_path: path, cwd: Dir.pwd).prepend_requires(source)
101
132
  end
102
133
  private_class_method :prepend_mrbgem_requires
103
134
 
@@ -157,7 +188,7 @@ module Rpremote
157
188
  board: defaults.fetch(:board, Target::DEFAULT_BOARD),
158
189
  cache_dir: defaults.fetch(:cache, Target::DEFAULT_CACHE_DIR),
159
190
  firmware: defaults[:firmware],
160
- mrbgems: defaults[:mrbgems],
191
+ mrbgems_lock: defaults[:mrbgems_lock],
161
192
  mount: defaults[:mount],
162
193
  port: defaults[:port],
163
194
  baud: defaults.fetch(:baud, Serial::BAUD_RATE),
@@ -175,8 +206,8 @@ module Rpremote
175
206
  parser.on("--board BOARD") { |value| options[:board] = value }
176
207
  parser.on("--cache DIR") { |value| options[:cache_dir] = value }
177
208
  parser.on("--firmware FILE") { |value| options[:firmware] = value }
178
- parser.on("--mrbgems FILE") { |value| options[:mrbgems] = value }
179
- parser.on("--no-mrbgems") { options[:mrbgems] = false }
209
+ parser.on("--lockfile FILE") { |value| options[:mrbgems_lock] = value }
210
+ parser.on("--no-mrbgems") { options[:mrbgems_lock] = false }
180
211
  parser.on("--mount DIR") { |value| options[:mount] = value }
181
212
  parser.on("--port PORT") { |value| options[:port] = value }
182
213
  parser.on("--baud RATE", Integer) { |value| options[:baud] = value }
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rpremote
4
+ module Help
5
+ def self.dfu_text(subcommand)
6
+ return dfu_app_text if subcommand == "app"
7
+ return dfu_compile_text if subcommand == "compile"
8
+ return dfu_status_text if subcommand == "status"
9
+ return dfu_remove_text if subcommand == "remove"
10
+
11
+ <<~HELP
12
+ Usage: #{COMMAND_USAGE.fetch(:dfu_app)}
13
+ #{COMMAND_USAGE.fetch(:dfu_compile)}
14
+ #{COMMAND_USAGE.fetch(:dfu_status)}
15
+ #{COMMAND_USAGE.fetch(:dfu_remove)}
16
+
17
+ Stages and inspects PicoModem DFU applications. Run `rpremote dfu SUBCOMMAND --help` for details.
18
+ HELP
19
+ end
20
+
21
+ def self.dfu_app_text
22
+ <<~HELP
23
+ Usage: #{COMMAND_USAGE.fetch(:dfu_app)}
24
+
25
+ Transfers a Ruby source or matching RITE bytecode application to the inactive DFU slot. The next R2P2 restart tries it.
26
+ Options default to the configured or automatically selected CDC 0 port, 115200 baud, and 20 seconds.
27
+ The transfer changes the staged boot application; use `rpremote reset` to restart and require DFU.confirm after a successful boot.
28
+ HELP
29
+ end
30
+
31
+ def self.dfu_compile_text
32
+ <<~HELP
33
+ Usage: #{COMMAND_USAGE.fetch(:dfu_compile)}
34
+
35
+ Compiles Ruby source to bytecode that matches the selected PicoRuby version. The output defaults beside FILE.
36
+ It requires prepared PicoRuby source and does not connect to a board.
37
+ HELP
38
+ end
39
+
40
+ def self.dfu_status_text
41
+ <<~HELP
42
+ Usage: #{COMMAND_USAGE.fetch(:dfu_status)}
43
+
44
+ Prints the active and candidate DFU A/B slots. Defaults are the configured or automatically selected CDC 0 port,
45
+ 115200 baud, and 20 seconds. This command reads board state without changing it.
46
+ HELP
47
+ end
48
+
49
+ def self.dfu_remove_text
50
+ <<~HELP
51
+ Usage: #{COMMAND_USAGE.fetch(:dfu_remove)}
52
+
53
+ Permanently removes the Ruby source and bytecode applications from both DFU A/B slots and resets their metadata.
54
+ Run `rpremote reset` afterward to stop the application already running in RAM. It does not remove /home/app.rb,
55
+ /home/app.mrb, other files, embedded mrbgems, or R2P2 firmware.
56
+ HELP
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rpremote
4
+ module Help
5
+ def self.fs_text(subcommand)
6
+ return fs_subcommand_text(subcommand) if %w[cp push cat ls rm mkdir].include?(subcommand)
7
+
8
+ <<~HELP
9
+ Usage: #{COMMAND_USAGE.fetch(:fs_cp)}
10
+ #{COMMAND_USAGE.fetch(:fs_push)}
11
+ #{COMMAND_USAGE.fetch(:fs_cat)}
12
+ #{COMMAND_USAGE.fetch(:fs_ls)}
13
+ #{COMMAND_USAGE.fetch(:fs_rm)}
14
+ #{COMMAND_USAGE.fetch(:fs_mkdir)}
15
+
16
+ Uses :/REMOTE/PATH for R2P2 paths. Run `rpremote fs SUBCOMMAND --help` for details.
17
+ HELP
18
+ end
19
+
20
+ def self.fs_subcommand_text(subcommand)
21
+ usage = COMMAND_USAGE.fetch(:"fs_#{subcommand}")
22
+ effect = case subcommand
23
+ when "rm" then "Deletes the remote path permanently."
24
+ when "mkdir" then "Creates a remote directory."
25
+ when "cp" then "Transfers one file, or recursively uploads a local directory with --recursive."
26
+ when "push" then "Creates missing remote directories and recursively uploads a local directory."
27
+ else "Reads remote files."
28
+ end
29
+ <<~HELP
30
+ Usage: #{usage}
31
+
32
+ #{effect} Defaults are the automatic CDC 0 port, 115200 baud, and 20 seconds.
33
+ HELP
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rpremote
4
+ module Help
5
+ def self.setup_text
6
+ <<~HELP
7
+ Usage: #{COMMAND_USAGE.fetch(:setup)}
8
+
9
+ Creates config/setting.json when it does not exist, then downloads and prepares PicoRuby source and the official Raspberry Pi nuke_universal.uf2 firmware.
10
+ Options: --language LANGUAGE (picoruby), --language-version VERSION (latest), --cache DIR (firmware), --force.
11
+ This changes the project configuration and source cache but does not connect to a board.
12
+ HELP
13
+ end
14
+
15
+ def self.build_text
16
+ <<~HELP
17
+ Usage: #{COMMAND_USAGE.fetch(:build)}
18
+
19
+ Builds a custom UF2 for the selected target. It writes generated files under build/ and the selected firmware path.
20
+ Options: --language LANGUAGE, --language-version VERSION, --board BOARD (pico2), --cache DIR (firmware),
21
+ --firmware FILE, --lockfile FILE, --no-mrbgems. A project Mrbgems.lock file is used automatically by default.
22
+ HELP
23
+ end
24
+
25
+ def self.build_clean_text
26
+ <<~HELP
27
+ Usage: #{COMMAND_USAGE.fetch(:build_clean)}
28
+
29
+ Removes only the project's generated build/ directory. It does not remove firmware/, Mrbgems, or Mrbgems.lock.
30
+ HELP
31
+ end
32
+
33
+ def self.deploy_text
34
+ <<~HELP
35
+ Usage: #{COMMAND_USAGE.fetch(:deploy)}
36
+
37
+ Enters BOOTSEL when needed, flashes the existing selected UF2, and waits for the R2P2 Shell to become ready.
38
+ With --build, builds the selected custom UF2 before entering BOOTSEL and flashing it.
39
+ If PATH/lib/NAME exists, it copies it to :/lib/NAME, where NAME is the final component of PATH. It then runs PATH/main.rb and preserves its Shell job, so hardware output remains active until the next command.
40
+ The stages run in order and stop at the first failure. Flashing replaces persistent board firmware.
41
+ deploy requires current PicoRuby 4.x firmware.
42
+ Options combine build, BOOTSEL, flash, library-copy, and run settings. The first install of firmware with automatic BOOTSEL still requires holding BOOTSEL.
43
+ HELP
44
+ end
45
+
46
+ def self.bootsel_text
47
+ <<~HELP
48
+ Usage: #{COMMAND_USAGE.fetch(:bootsel)}
49
+
50
+ Asks the running R2P2 firmware to restart as an RP2350 BOOTSEL USB volume, then waits for that volume to appear.
51
+ By default it does not write firmware. This requires firmware built by a current rpremote; use physical BOOTSEL once to install it first.
52
+ --reset-flash-memory copies the official Raspberry Pi nuke_universal.uf2 in BOOTSEL mode and erases all external flash memory.
53
+ After resetting flash memory, wait for BOOTSEL to reappear and run `rpremote flash` to install R2P2 again.
54
+ HELP
55
+ end
56
+
57
+ def self.mrbgems_text(subcommand)
58
+ action = subcommand || "check|list|lock|update"
59
+ <<~HELP
60
+ Usage: #{COMMAND_USAGE.fetch(:mrbgems).sub("SUBCOMMAND", action)}
61
+
62
+ Checks or lists build dependencies, writes a reproducible lock file, or updates locked GitHub commits.
63
+ `lock --with production --without test` includes common and production gems. Exclusions take precedence.
64
+ `update` resolves new commits using the same group options.
65
+ HELP
66
+ end
67
+
68
+ def self.flash_text
69
+ <<~HELP
70
+ Usage: #{COMMAND_USAGE.fetch(:flash)}
71
+
72
+ Copies the selected UF2 to an RP2350 BOOTSEL volume and waits for R2P2 to reconnect. It replaces persistent board firmware.
73
+ Defaults select pico2, firmware/picoruby-latest-pico2.uf2, an automatic BOOTSEL mount and CDC 0 port, and 20 seconds.
74
+ HELP
75
+ end
76
+
77
+ def self.config_text(subcommand)
78
+ unless subcommand == "show"
79
+ return "Usage: #{COMMAND_USAGE.fetch(:config_show)}\n\nUse `rpremote config show --help` for the supported overrides.\n"
80
+ end
81
+
82
+ <<~HELP
83
+ Usage: #{COMMAND_USAGE.fetch(:config_show)}
84
+
85
+ Prints the configuration file, defaults, and command-line overrides after resolution. It does not connect to a board or change state.
86
+ HELP
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rpremote
4
+ module Help
5
+ def self.ports_text
6
+ "Usage: #{COMMAND_USAGE.fetch(:ports)}\n\nPrints each detected R2P2 CDC 0 serial path, one per line. It does not connect to a board.\n"
7
+ end
8
+
9
+ def self.run_text
10
+ <<~HELP
11
+ Usage: #{COMMAND_USAGE.fetch(:run)}
12
+
13
+ Uploads FILE to R2P2, or main.rb when FILE is a directory, relays output, then removes the temporary remote file. Defaults are the automatic CDC 0 port,
14
+ 115200 baud, a 20-second idle timeout, and picoruby. Output from the running program resets the idle timeout.
15
+ It exits nonzero when compatible R2P2 firmware reports a Ruby exception.
16
+ --reset-on-timeout resets R2P2 after the run times out and the serial connection has closed.
17
+ HELP
18
+ end
19
+
20
+ def self.exec_text
21
+ <<~HELP
22
+ Usage: #{COMMAND_USAGE.fetch(:exec)}
23
+
24
+ Runs short Ruby CODE through a temporary remote file, then removes it. Defaults are the automatic CDC 0 port,
25
+ 115200 baud, a 20-second idle timeout, and picoruby. Output from the running program resets the idle timeout.
26
+ It exits nonzero when compatible R2P2 firmware reports a Ruby exception.
27
+ HELP
28
+ end
29
+
30
+ def self.monitor_text
31
+ <<~HELP
32
+ Usage: #{COMMAND_USAGE.fetch(:monitor)}
33
+
34
+ Opens a serial monitor for the selected CDC 0 port. Defaults are 115200 baud and 20 seconds; exit with Ctrl-].
35
+ HELP
36
+ end
37
+
38
+ def self.repl_text
39
+ <<~HELP
40
+ Usage: #{COMMAND_USAGE.fetch(:repl)}
41
+
42
+ Opens PicoIRB on the selected CDC 0 port. Defaults are 115200 baud and 20 seconds; exit with Ctrl-].
43
+ HELP
44
+ end
45
+
46
+ def self.reset_text
47
+ <<~HELP
48
+ Usage: #{COMMAND_USAGE.fetch(:reset)}
49
+
50
+ Reboots R2P2 and waits for reconnection. Defaults are the automatic CDC 0 port, 115200 baud, and 20 seconds.
51
+ HELP
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rpremote
4
+ module Help
5
+ TEXT = <<~HELP.freeze
6
+ rpremote - R2P2 remote control for Raspberry Pi Pico 2
7
+
8
+ Usage:
9
+ #{COMMAND_USAGE.fetch(:setup)}
10
+ #{COMMAND_USAGE.fetch(:build)}
11
+ #{COMMAND_USAGE.fetch(:build_clean)}
12
+ #{COMMAND_USAGE.fetch(:bootsel)}
13
+ #{COMMAND_USAGE.fetch(:deploy)}
14
+ #{COMMAND_USAGE.fetch(:dfu_app)}
15
+ #{COMMAND_USAGE.fetch(:dfu_compile)}
16
+ #{COMMAND_USAGE.fetch(:dfu_status)}
17
+ #{COMMAND_USAGE.fetch(:dfu_remove)}
18
+ #{COMMAND_USAGE.fetch(:mrbgems).sub("SUBCOMMAND", "check|list|lock|update")}
19
+ #{COMMAND_USAGE.fetch(:flash)}
20
+ #{COMMAND_USAGE.fetch(:config_show)}
21
+ #{COMMAND_USAGE.fetch(:ports)}
22
+ #{COMMAND_USAGE.fetch(:run)}
23
+ #{COMMAND_USAGE.fetch(:monitor)}
24
+ #{COMMAND_USAGE.fetch(:repl)}
25
+ #{COMMAND_USAGE.fetch(:exec)}
26
+ #{COMMAND_USAGE.fetch(:reset)}
27
+ #{COMMAND_USAGE.fetch(:fs_cp)}
28
+ #{COMMAND_USAGE.fetch(:fs_push)}
29
+ #{COMMAND_USAGE.fetch(:fs_cat)}
30
+ #{COMMAND_USAGE.fetch(:fs_ls)}
31
+ #{COMMAND_USAGE.fetch(:fs_rm)}
32
+ #{COMMAND_USAGE.fetch(:fs_mkdir)}
33
+
34
+ `rpremote build clean` removes only the project's generated `build/` directory.
35
+
36
+ Configuration:
37
+ config/setting.json default project options
38
+
39
+ Options:
40
+ --force download the PicoRuby source again during setup
41
+ --build build the selected firmware before deploy flashes it
42
+ --language-version VERSION
43
+ use a PicoRuby tag or latest (default: latest)
44
+ --cache DIR use another project cache directory
45
+ --lockfile FILE use an explicit Mrbgems.lock for build, deploy, run, or exec
46
+ --no-mrbgems do not build or automatically require locked mrbgems
47
+ --with GROUPS lock common mrbgems and the comma-separated groups
48
+ --without GROUPS exclude the comma-separated groups from the lock
49
+ --board BOARD
50
+ select pico2 or pico2_w (default: pico2)
51
+ --firmware FILE build to, or deploy or flash from, this UF2 path
52
+ --language LANGUAGE
53
+ select the remote language (default: picoruby)
54
+ --config FILE use another configuration file
55
+ --mount DIR use an explicit RP2350 BOOTSEL drive
56
+ --port PORT use an explicit R2P2 CDC 0 device
57
+ --baud RATE serial baud rate (default: 115200)
58
+ --timeout SEC timeout in seconds (default: 20)
59
+ -h, --help show this help
60
+ -V, --version show the version
61
+ HELP
62
+ end
63
+ end