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.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/.obsidian/app.json +6 -0
  3. data/.obsidian/appearance.json +4 -0
  4. data/.obsidian/community-plugins.json +5 -0
  5. data/.obsidian/core-plugins.json +33 -0
  6. data/.obsidian/plugins/obsidian-minimal-settings/data.json +34 -0
  7. data/.obsidian/plugins/obsidian-minimal-settings/main.js +8 -0
  8. data/.obsidian/plugins/obsidian-minimal-settings/manifest.json +11 -0
  9. data/.obsidian/plugins/obsidian-style-settings/data.json +15 -0
  10. data/.obsidian/plugins/obsidian-style-settings/main.js +165 -0
  11. data/.obsidian/plugins/obsidian-style-settings/manifest.json +10 -0
  12. data/.obsidian/plugins/obsidian-style-settings/styles.css +243 -0
  13. data/.obsidian/plugins/table-editor-obsidian/data.json +6 -0
  14. data/.obsidian/plugins/table-editor-obsidian/main.js +236 -0
  15. data/.obsidian/plugins/table-editor-obsidian/manifest.json +17 -0
  16. data/.obsidian/plugins/table-editor-obsidian/styles.css +78 -0
  17. data/.obsidian/themes/AnuPpuccin/manifest.json +7 -0
  18. data/.obsidian/themes/AnuPpuccin/theme.css +9080 -0
  19. data/.obsidian/themes/Minimal/manifest.json +8 -0
  20. data/.obsidian/themes/Minimal/theme.css +2251 -0
  21. data/.rubocop.yml +231 -0
  22. data/CHANGELOG.md +97 -0
  23. data/LICENSE +21 -0
  24. data/README.md +314 -0
  25. data/Rakefile +13 -0
  26. data/devex-logo.jpg +0 -0
  27. data/docs/developing-tools.md +1000 -0
  28. data/docs/ref/agent-mode.md +46 -0
  29. data/docs/ref/cli-interface.md +60 -0
  30. data/docs/ref/configuration.md +46 -0
  31. data/docs/ref/design-philosophy.md +17 -0
  32. data/docs/ref/error-handling.md +38 -0
  33. data/docs/ref/io-handling.md +88 -0
  34. data/docs/ref/signals.md +141 -0
  35. data/docs/ref/temporal-software-theory.md +790 -0
  36. data/exe/dx +52 -0
  37. data/lib/devex/builtins/.index.rb +10 -0
  38. data/lib/devex/builtins/debug.rb +43 -0
  39. data/lib/devex/builtins/format.rb +44 -0
  40. data/lib/devex/builtins/gem.rb +77 -0
  41. data/lib/devex/builtins/lint.rb +61 -0
  42. data/lib/devex/builtins/test.rb +76 -0
  43. data/lib/devex/builtins/version.rb +156 -0
  44. data/lib/devex/cli.rb +340 -0
  45. data/lib/devex/context.rb +433 -0
  46. data/lib/devex/core/configuration.rb +136 -0
  47. data/lib/devex/core.rb +79 -0
  48. data/lib/devex/dirs.rb +210 -0
  49. data/lib/devex/dsl.rb +100 -0
  50. data/lib/devex/exec/controller.rb +245 -0
  51. data/lib/devex/exec/result.rb +229 -0
  52. data/lib/devex/exec.rb +662 -0
  53. data/lib/devex/loader.rb +136 -0
  54. data/lib/devex/output.rb +257 -0
  55. data/lib/devex/project_paths.rb +309 -0
  56. data/lib/devex/support/ansi.rb +437 -0
  57. data/lib/devex/support/core_ext.rb +560 -0
  58. data/lib/devex/support/global.rb +68 -0
  59. data/lib/devex/support/path.rb +357 -0
  60. data/lib/devex/support.rb +71 -0
  61. data/lib/devex/template_helpers.rb +136 -0
  62. data/lib/devex/templates/debug.erb +24 -0
  63. data/lib/devex/tool.rb +374 -0
  64. data/lib/devex/version.rb +5 -0
  65. data/lib/devex/working_dir.rb +99 -0
  66. data/lib/devex.rb +158 -0
  67. data/ruby-project-template/.gitignore +0 -0
  68. data/ruby-project-template/Gemfile +0 -0
  69. data/ruby-project-template/README.md +0 -0
  70. data/ruby-project-template/docs/README.md +0 -0
  71. data/sig/devex.rbs +4 -0
  72. 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