devex 0.3.5
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/.obsidian/app.json +6 -0
- data/.obsidian/appearance.json +4 -0
- data/.obsidian/community-plugins.json +5 -0
- data/.obsidian/core-plugins.json +33 -0
- data/.obsidian/plugins/obsidian-minimal-settings/data.json +34 -0
- data/.obsidian/plugins/obsidian-minimal-settings/main.js +8 -0
- data/.obsidian/plugins/obsidian-minimal-settings/manifest.json +11 -0
- data/.obsidian/plugins/obsidian-style-settings/data.json +15 -0
- data/.obsidian/plugins/obsidian-style-settings/main.js +165 -0
- data/.obsidian/plugins/obsidian-style-settings/manifest.json +10 -0
- data/.obsidian/plugins/obsidian-style-settings/styles.css +243 -0
- data/.obsidian/plugins/table-editor-obsidian/data.json +6 -0
- data/.obsidian/plugins/table-editor-obsidian/main.js +236 -0
- data/.obsidian/plugins/table-editor-obsidian/manifest.json +17 -0
- data/.obsidian/plugins/table-editor-obsidian/styles.css +78 -0
- data/.obsidian/themes/AnuPpuccin/manifest.json +7 -0
- data/.obsidian/themes/AnuPpuccin/theme.css +9080 -0
- data/.obsidian/themes/Minimal/manifest.json +8 -0
- data/.obsidian/themes/Minimal/theme.css +2251 -0
- data/.rubocop.yml +231 -0
- data/CHANGELOG.md +97 -0
- data/LICENSE +21 -0
- data/README.md +314 -0
- data/Rakefile +13 -0
- data/devex-logo.jpg +0 -0
- data/docs/developing-tools.md +1000 -0
- data/docs/ref/agent-mode.md +46 -0
- data/docs/ref/cli-interface.md +60 -0
- data/docs/ref/configuration.md +46 -0
- data/docs/ref/design-philosophy.md +17 -0
- data/docs/ref/error-handling.md +38 -0
- data/docs/ref/io-handling.md +88 -0
- data/docs/ref/signals.md +141 -0
- data/docs/ref/temporal-software-theory.md +790 -0
- data/exe/dx +52 -0
- data/lib/devex/builtins/.index.rb +10 -0
- data/lib/devex/builtins/debug.rb +43 -0
- data/lib/devex/builtins/format.rb +44 -0
- data/lib/devex/builtins/gem.rb +77 -0
- data/lib/devex/builtins/lint.rb +61 -0
- data/lib/devex/builtins/test.rb +76 -0
- data/lib/devex/builtins/version.rb +156 -0
- data/lib/devex/cli.rb +340 -0
- data/lib/devex/context.rb +433 -0
- data/lib/devex/core/configuration.rb +136 -0
- data/lib/devex/core.rb +79 -0
- data/lib/devex/dirs.rb +210 -0
- data/lib/devex/dsl.rb +100 -0
- data/lib/devex/exec/controller.rb +245 -0
- data/lib/devex/exec/result.rb +229 -0
- data/lib/devex/exec.rb +662 -0
- data/lib/devex/loader.rb +136 -0
- data/lib/devex/output.rb +257 -0
- data/lib/devex/project_paths.rb +309 -0
- data/lib/devex/support/ansi.rb +437 -0
- data/lib/devex/support/core_ext.rb +560 -0
- data/lib/devex/support/global.rb +68 -0
- data/lib/devex/support/path.rb +357 -0
- data/lib/devex/support.rb +71 -0
- data/lib/devex/template_helpers.rb +136 -0
- data/lib/devex/templates/debug.erb +24 -0
- data/lib/devex/tool.rb +374 -0
- data/lib/devex/version.rb +5 -0
- data/lib/devex/working_dir.rb +99 -0
- data/lib/devex.rb +158 -0
- data/ruby-project-template/.gitignore +0 -0
- data/ruby-project-template/Gemfile +0 -0
- data/ruby-project-template/README.md +0 -0
- data/ruby-project-template/docs/README.md +0 -0
- data/sig/devex.rbs +4 -0
- metadata +122 -0
data/exe/dx
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../lib/devex"
|
|
5
|
+
|
|
6
|
+
# Extract --dx-from-dir before project discovery
|
|
7
|
+
# This flag affects where we look for the project root
|
|
8
|
+
argv = ARGV.dup
|
|
9
|
+
from_dir = nil
|
|
10
|
+
argv.reject! do |arg|
|
|
11
|
+
if arg.start_with?("--dx-from-dir=")
|
|
12
|
+
from_dir = arg.split("=", 2).last
|
|
13
|
+
true
|
|
14
|
+
elsif arg == "--dx-from-dir" && argv.index(arg) < argv.size - 1
|
|
15
|
+
# Handle --dx-from-dir PATH (space-separated)
|
|
16
|
+
idx = argv.index(arg)
|
|
17
|
+
from_dir = argv[idx + 1]
|
|
18
|
+
begin
|
|
19
|
+
argv.delete_at(idx + 1)
|
|
20
|
+
rescue StandardError
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
true
|
|
24
|
+
else
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Set dest_dir if --dx-from-dir was provided
|
|
30
|
+
Devex::Dirs.dest_dir = File.expand_path(from_dir) if from_dir
|
|
31
|
+
|
|
32
|
+
# Check for .dx-use-local and delegate to bundled dx if present
|
|
33
|
+
# This ensures version consistency when a project pins a specific devex version
|
|
34
|
+
Devex::Dirs.maybe_delegate_to_local!(config: Devex::DX_CONFIG, argv: argv)
|
|
35
|
+
|
|
36
|
+
# Create CLI with dx configuration
|
|
37
|
+
cli = Devex::CLI.new(config: Devex::DX_CONFIG)
|
|
38
|
+
|
|
39
|
+
# Load built-in tools
|
|
40
|
+
cli.load_builtins
|
|
41
|
+
|
|
42
|
+
# Find project root and load project tools
|
|
43
|
+
# Use from_dir if provided, otherwise search from current directory
|
|
44
|
+
search_from = from_dir ? File.expand_path(from_dir) : Dir.pwd
|
|
45
|
+
project_root, _marker = Devex.find_project_root(search_from)
|
|
46
|
+
cli.load_project_tools(project_root) if project_root
|
|
47
|
+
|
|
48
|
+
# Merge builtins (project takes precedence)
|
|
49
|
+
cli.merge_builtins
|
|
50
|
+
|
|
51
|
+
# Run
|
|
52
|
+
exit cli.run(argv)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Devex built-in tools root configuration
|
|
4
|
+
|
|
5
|
+
desc "Developer experience CLI"
|
|
6
|
+
long_desc <<~DESC
|
|
7
|
+
devex provides common development tasks for Ruby projects.
|
|
8
|
+
|
|
9
|
+
Run 'dx help COMMAND' for more information on a specific command.
|
|
10
|
+
DESC
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Hidden from help - useful for debugging context detection issues
|
|
4
|
+
# Access via: dx debug
|
|
5
|
+
|
|
6
|
+
desc "Debug context detection"
|
|
7
|
+
|
|
8
|
+
def run
|
|
9
|
+
ctx = Devex::Context
|
|
10
|
+
|
|
11
|
+
data = {
|
|
12
|
+
tty: {
|
|
13
|
+
stdout: $stdout.tty?,
|
|
14
|
+
stderr: $stderr.tty?,
|
|
15
|
+
stdin: $stdin.tty?,
|
|
16
|
+
terminal: ctx.terminal?
|
|
17
|
+
},
|
|
18
|
+
streams: {
|
|
19
|
+
merged: ctx.streams_merged?,
|
|
20
|
+
piped: ctx.piped?
|
|
21
|
+
},
|
|
22
|
+
environment: {
|
|
23
|
+
ci: ctx.ci?,
|
|
24
|
+
env: ctx.env,
|
|
25
|
+
agent_mode_env: ctx.agent_mode_env?,
|
|
26
|
+
dx_agent_mode: ENV.fetch("DX_AGENT_MODE", nil),
|
|
27
|
+
devex_agent_mode: ENV.fetch("DEVEX_AGENT_MODE", nil)
|
|
28
|
+
},
|
|
29
|
+
detection: {
|
|
30
|
+
agent_mode: ctx.agent_mode?,
|
|
31
|
+
interactive: ctx.interactive?,
|
|
32
|
+
color: ctx.color?
|
|
33
|
+
},
|
|
34
|
+
call_tree: ctx.call_tree,
|
|
35
|
+
overrides: ctx.overrides
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
case output_format
|
|
39
|
+
when :json, :yaml then Devex::Output.data(data, format: output_format)
|
|
40
|
+
else
|
|
41
|
+
$stdout.print Devex.render_template("debug", data)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Uses prj.linter - fails fast with helpful message if not found.
|
|
4
|
+
|
|
5
|
+
desc "Auto-format code"
|
|
6
|
+
|
|
7
|
+
long_desc <<~DESC
|
|
8
|
+
Auto-formats code using your linter's fix mode.
|
|
9
|
+
Equivalent to `dx lint --fix`.
|
|
10
|
+
|
|
11
|
+
Supports:
|
|
12
|
+
- RuboCop (.rubocop.yml) - runs rubocop -a
|
|
13
|
+
- StandardRB (.standard.yml) - runs standardrb --fix
|
|
14
|
+
DESC
|
|
15
|
+
|
|
16
|
+
flag :unsafe, "-A", "--unsafe", desc: "Include unsafe corrections"
|
|
17
|
+
remaining_args :files, desc: "Specific files or patterns to format"
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
# prj.linter fails fast if no .rubocop.yml or .standard.yml found
|
|
21
|
+
linter_config = prj.linter
|
|
22
|
+
|
|
23
|
+
case linter_config.basename.to_s
|
|
24
|
+
when ".standard.yml" then run_standardrb
|
|
25
|
+
when ".rubocop.yml" then run_rubocop
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def prj = @prj ||= Devex::ProjectPaths.new
|
|
30
|
+
|
|
31
|
+
def run_rubocop
|
|
32
|
+
args = ["rubocop"]
|
|
33
|
+
args << (unsafe ? "-A" : "-a")
|
|
34
|
+
args += files unless files.empty?
|
|
35
|
+
|
|
36
|
+
cmd(*args, chdir: prj.root).exit_on_failure!
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def run_standardrb
|
|
40
|
+
args = ["standardrb", "--fix"]
|
|
41
|
+
args += files unless files.empty?
|
|
42
|
+
|
|
43
|
+
cmd(*args, chdir: prj.root).exit_on_failure!
|
|
44
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Uses prj.gemspec - fails fast with helpful message if not found.
|
|
4
|
+
|
|
5
|
+
desc "Gem packaging tasks"
|
|
6
|
+
|
|
7
|
+
long_desc <<~DESC
|
|
8
|
+
Build and manage gem packages.
|
|
9
|
+
|
|
10
|
+
Subcommands:
|
|
11
|
+
dx gem build - Build the gem (.gem file)
|
|
12
|
+
dx gem install - Build and install locally
|
|
13
|
+
dx gem clean - Remove built gem files
|
|
14
|
+
DESC
|
|
15
|
+
|
|
16
|
+
tool "build" do
|
|
17
|
+
desc "Build the gem"
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
cmd("gem", "build", prj.gemspec.basename, chdir: prj.root).exit_on_failure!
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def prj = @prj ||= Devex::ProjectPaths.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
tool "install" do
|
|
27
|
+
desc "Build and install gem locally"
|
|
28
|
+
|
|
29
|
+
def run
|
|
30
|
+
$stdout.puts "Building gem..."
|
|
31
|
+
cmd("gem", "build", prj.gemspec.basename, chdir: prj.root)
|
|
32
|
+
.then { install_built_gem }
|
|
33
|
+
.exit_on_failure!
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def install_built_gem
|
|
37
|
+
gem_file = prj.root.glob("*.gem").max_by(&:mtime)
|
|
38
|
+
unless gem_file
|
|
39
|
+
$stderr.puts "Build succeeded but no .gem file found"
|
|
40
|
+
exit 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
$stdout.puts "Installing #{gem_file.basename}..."
|
|
44
|
+
result = cmd("gem", "install", gem_file.basename, "--local", chdir: prj.root)
|
|
45
|
+
|
|
46
|
+
if result.success?
|
|
47
|
+
gem_file.rm
|
|
48
|
+
$stdout.puts "Installed and cleaned up."
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
result
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def prj = @prj ||= Devex::ProjectPaths.new
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
tool "clean" do
|
|
58
|
+
desc "Remove built gem files"
|
|
59
|
+
|
|
60
|
+
def run
|
|
61
|
+
gem_files = prj.root.glob("*.gem")
|
|
62
|
+
if gem_files.empty?
|
|
63
|
+
$stdout.puts "No .gem files to clean"
|
|
64
|
+
else
|
|
65
|
+
gem_files.each do |f|
|
|
66
|
+
f.rm
|
|
67
|
+
$stdout.puts "Removed #{f.basename}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def prj = @prj ||= Devex::ProjectPaths.new
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def run
|
|
76
|
+
cli.show_help(tool)
|
|
77
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Uses prj.linter - fails fast with helpful message if not found.
|
|
4
|
+
|
|
5
|
+
desc "Run linter"
|
|
6
|
+
|
|
7
|
+
long_desc <<~DESC
|
|
8
|
+
Auto-detects and runs your linter from the project root.
|
|
9
|
+
|
|
10
|
+
Supports:
|
|
11
|
+
- RuboCop (.rubocop.yml)
|
|
12
|
+
- StandardRB (.standard.yml)
|
|
13
|
+
|
|
14
|
+
Pass additional arguments after -- to forward to the linter:
|
|
15
|
+
dx lint -- --only=Style/StringLiterals
|
|
16
|
+
DESC
|
|
17
|
+
|
|
18
|
+
flag :fix, "-a", "--fix", desc: "Auto-fix correctable offenses"
|
|
19
|
+
flag :unsafe, "-A", "--unsafe-fix", desc: "Auto-fix including unsafe corrections"
|
|
20
|
+
flag :diff, "-d", "--diff", desc: "Only lint changed files (git diff)"
|
|
21
|
+
remaining_args :files, desc: "Specific files or patterns to lint"
|
|
22
|
+
|
|
23
|
+
def run
|
|
24
|
+
# prj.linter fails fast if no .rubocop.yml or .standard.yml found
|
|
25
|
+
linter_config = prj.linter
|
|
26
|
+
|
|
27
|
+
case linter_config.basename.to_s
|
|
28
|
+
when ".standard.yml" then run_standardrb
|
|
29
|
+
when ".rubocop.yml" then run_rubocop
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def prj = @prj ||= Devex::ProjectPaths.new
|
|
34
|
+
|
|
35
|
+
def run_rubocop
|
|
36
|
+
args = ["rubocop"]
|
|
37
|
+
args << "-a" if fix && !unsafe
|
|
38
|
+
args << "-A" if unsafe
|
|
39
|
+
args += changed_files if diff && files.empty?
|
|
40
|
+
args += files unless files.empty?
|
|
41
|
+
|
|
42
|
+
cmd(*args, chdir: prj.root).exit_on_failure!
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def run_standardrb
|
|
46
|
+
args = ["standardrb"]
|
|
47
|
+
args << "--fix" if fix || unsafe
|
|
48
|
+
args += changed_files if diff && files.empty?
|
|
49
|
+
args += files unless files.empty?
|
|
50
|
+
|
|
51
|
+
cmd(*args, chdir: prj.root).exit_on_failure!
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def changed_files
|
|
55
|
+
result = capture("git", "diff", "--name-only", "--diff-filter=ACMR", "HEAD", chdir: prj.root)
|
|
56
|
+
return [] if result.failed?
|
|
57
|
+
|
|
58
|
+
result.stdout_lines
|
|
59
|
+
.select { |f| f.end_with?(".rb") }
|
|
60
|
+
.select { |f| (prj.root / f).exist? }
|
|
61
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Uses prj.test - fails fast with helpful message if not found.
|
|
4
|
+
|
|
5
|
+
desc "Run tests"
|
|
6
|
+
|
|
7
|
+
long_desc <<~DESC
|
|
8
|
+
Auto-detects and runs your test suite from the project root.
|
|
9
|
+
|
|
10
|
+
Supports:
|
|
11
|
+
- Minitest (test/ directory)
|
|
12
|
+
- RSpec (spec/ directory or .rspec file)
|
|
13
|
+
|
|
14
|
+
Pass additional arguments after -- to forward to the test runner:
|
|
15
|
+
dx test -- --seed=12345
|
|
16
|
+
dx test -- spec/models/user_spec.rb
|
|
17
|
+
DESC
|
|
18
|
+
|
|
19
|
+
flag :coverage, "-c", "--coverage", desc: "Run with coverage (sets COVERAGE=1)"
|
|
20
|
+
flag :fail_fast, "--fail-fast", desc: "Stop on first failure"
|
|
21
|
+
remaining_args :files, desc: "Specific test files or patterns"
|
|
22
|
+
|
|
23
|
+
def run
|
|
24
|
+
# prj.test fails fast if no test/, spec/, or tests/ directory exists
|
|
25
|
+
test_dir = prj.test
|
|
26
|
+
env = coverage ? { "COVERAGE" => "1" } : {}
|
|
27
|
+
|
|
28
|
+
# Determine framework from directory name
|
|
29
|
+
case test_dir.basename.to_s
|
|
30
|
+
when "spec"
|
|
31
|
+
run_rspec(env)
|
|
32
|
+
when "test", "tests"
|
|
33
|
+
run_minitest(env)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def prj = @prj ||= Devex::ProjectPaths.new
|
|
38
|
+
|
|
39
|
+
def run_minitest(env)
|
|
40
|
+
if files.empty?
|
|
41
|
+
rakefile = prj.root / "Rakefile"
|
|
42
|
+
if rakefile.exist? && rake_has_test_task?(rakefile)
|
|
43
|
+
cmd("rake", "test", env: env, chdir: prj.root).exit_on_failure!
|
|
44
|
+
else
|
|
45
|
+
test_files = prj.test.glob("**/*_test.rb")
|
|
46
|
+
if test_files.empty?
|
|
47
|
+
$stderr.puts "No test files found in #{prj.test}"
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
relative_files = test_files.map { |f| f.relative_path_from(prj.root) }
|
|
52
|
+
cmd("ruby", "-Itest", "-Ilib", "-e",
|
|
53
|
+
relative_files.map { |f| "require './#{f}'" }.join("; "),
|
|
54
|
+
env: env, chdir: prj.root).exit_on_failure!
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
files.each do |file|
|
|
58
|
+
cmd("ruby", "-Itest", "-Ilib", file, env: env, chdir: prj.root).exit_on_failure!
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def run_rspec(env)
|
|
64
|
+
args = ["rspec"]
|
|
65
|
+
args << "--fail-fast" if fail_fast
|
|
66
|
+
args += files unless files.empty?
|
|
67
|
+
|
|
68
|
+
cmd(*args, env: env, chdir: prj.root).exit_on_failure!
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def rake_has_test_task?(rakefile)
|
|
72
|
+
content = rakefile.read
|
|
73
|
+
content.include?("TestTask") || content.include?("task :test") || content.include?("task 'test'")
|
|
74
|
+
rescue StandardError
|
|
75
|
+
false
|
|
76
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shared helpers - these are defined at the top level and available to all tools
|
|
4
|
+
# when the file is re-evaluated in the execution context
|
|
5
|
+
|
|
6
|
+
VERSION_FILE_PATTERNS = [
|
|
7
|
+
"lib/*/version.rb",
|
|
8
|
+
"VERSION",
|
|
9
|
+
"version.rb"
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
VERSION_REGEX = /VERSION\s*=\s*["']([^"']+)["']/
|
|
13
|
+
|
|
14
|
+
def find_version_file(root)
|
|
15
|
+
VERSION_FILE_PATTERNS.each do |pattern|
|
|
16
|
+
matches = Dir.glob(File.join(root, pattern))
|
|
17
|
+
return matches.first if matches.any?
|
|
18
|
+
end
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def read_version(file)
|
|
23
|
+
content = File.read(file)
|
|
24
|
+
if content =~ VERSION_REGEX
|
|
25
|
+
Regexp.last_match(1)
|
|
26
|
+
else
|
|
27
|
+
content.strip
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def write_version(file, old_version, new_version)
|
|
32
|
+
content = File.read(file)
|
|
33
|
+
new_content = content.gsub(
|
|
34
|
+
/VERSION\s*=\s*["']#{Regexp.escape(old_version)}["']/,
|
|
35
|
+
%(VERSION = "#{new_version}")
|
|
36
|
+
)
|
|
37
|
+
File.write(file, new_content)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def bump_version(version, type)
|
|
41
|
+
parts = version.split(".").map(&:to_i)
|
|
42
|
+
parts = [0, 0, 0] if parts.length < 3
|
|
43
|
+
|
|
44
|
+
case type
|
|
45
|
+
when "major"
|
|
46
|
+
parts[0] += 1
|
|
47
|
+
parts[1] = 0
|
|
48
|
+
parts[2] = 0
|
|
49
|
+
when "minor"
|
|
50
|
+
parts[1] += 1
|
|
51
|
+
parts[2] = 0
|
|
52
|
+
when "patch" then parts[2] += 1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
parts.join(".")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def version_error(message)
|
|
59
|
+
Devex::Output.error(message)
|
|
60
|
+
exit(1)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def version_output(data)
|
|
64
|
+
# Use output_format from ExecutionContext (handles global + tool flags + context default)
|
|
65
|
+
fmt = respond_to?(:output_format) ? output_format : :text
|
|
66
|
+
|
|
67
|
+
case fmt
|
|
68
|
+
when :json, :yaml then Devex::Output.data(data, format: fmt)
|
|
69
|
+
else
|
|
70
|
+
text = if data[:old_version] && data[:new_version]
|
|
71
|
+
"#{data[:old_version]} → #{data[:new_version]}"
|
|
72
|
+
else
|
|
73
|
+
data[:version].to_s
|
|
74
|
+
end
|
|
75
|
+
$stdout.print text, "\n"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# --- Main tool ---
|
|
80
|
+
|
|
81
|
+
desc "Show or manage version"
|
|
82
|
+
|
|
83
|
+
def run
|
|
84
|
+
# Try project root first, then fall back to effective working directory.
|
|
85
|
+
# This handles cases where a project exists but isn't yet recognized
|
|
86
|
+
# (e.g., no .git, Gemfile, or other project markers).
|
|
87
|
+
fallback_dir = Devex::Dirs.dest_dir.to_s
|
|
88
|
+
search_roots = [cli.project_root, fallback_dir].compact.uniq
|
|
89
|
+
|
|
90
|
+
version_data = nil
|
|
91
|
+
search_roots.each do |root|
|
|
92
|
+
version_file = find_version_file(root)
|
|
93
|
+
if version_file
|
|
94
|
+
version_data = { version: read_version(version_file), source: "project", file: version_file }
|
|
95
|
+
break
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
version_data ||= { version: Devex::VERSION, source: "devex" }
|
|
100
|
+
|
|
101
|
+
version_output(version_data)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# --- Subtools ---
|
|
105
|
+
|
|
106
|
+
tool "bump" do
|
|
107
|
+
desc "Bump version (major, minor, or patch)"
|
|
108
|
+
long_desc <<~DESC
|
|
109
|
+
Bump the project version following semantic versioning.
|
|
110
|
+
|
|
111
|
+
MAJOR version for incompatible API changes
|
|
112
|
+
MINOR version for backwards-compatible new functionality
|
|
113
|
+
PATCH version for backwards-compatible bug fixes
|
|
114
|
+
DESC
|
|
115
|
+
|
|
116
|
+
required_arg :type, desc: "Version component: major, minor, or patch"
|
|
117
|
+
|
|
118
|
+
def run
|
|
119
|
+
version_error("Invalid version type '#{type}'. Use: major, minor, or patch") unless %w[major minor patch].include?(type)
|
|
120
|
+
|
|
121
|
+
version_error("Not in a project directory") unless cli.project_root
|
|
122
|
+
|
|
123
|
+
version_file = find_version_file(cli.project_root)
|
|
124
|
+
version_error("Could not find version file") unless version_file
|
|
125
|
+
|
|
126
|
+
old_version = read_version(version_file)
|
|
127
|
+
new_version = bump_version(old_version, type)
|
|
128
|
+
write_version(version_file, old_version, new_version)
|
|
129
|
+
|
|
130
|
+
version_output(
|
|
131
|
+
{ old_version: old_version, new_version: new_version, type: type, file: version_file }
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
tool "set" do
|
|
137
|
+
desc "Set version to a specific value"
|
|
138
|
+
|
|
139
|
+
required_arg :version, desc: "Version string (e.g., 1.0.0)"
|
|
140
|
+
|
|
141
|
+
def run
|
|
142
|
+
version_error("Invalid version format '#{version}'. Use semantic versioning: MAJOR.MINOR.PATCH") unless version.match?(/^\d+\.\d+\.\d+/)
|
|
143
|
+
|
|
144
|
+
version_error("Not in a project directory") unless cli.project_root
|
|
145
|
+
|
|
146
|
+
version_file = find_version_file(cli.project_root)
|
|
147
|
+
version_error("Could not find version file") unless version_file
|
|
148
|
+
|
|
149
|
+
old_version = read_version(version_file)
|
|
150
|
+
write_version(version_file, old_version, version)
|
|
151
|
+
|
|
152
|
+
version_output(
|
|
153
|
+
{ old_version: old_version, new_version: version, file: version_file }
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
end
|