dev_context 1.0.1 → 1.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 230f85766b6c9c36e855be5be7db4b6c395e813a392d7a4bb55f8f9e5ae43917
4
- data.tar.gz: f5fe1a5d23f665c60c2e62f44239ca74bf058bd6eadcb1b93cadccb35c0a6f36
3
+ metadata.gz: 5acaef7b6cb13adb292bcdbc2f59bff0a659517d3b4d17f36f23b8afb11ee40d
4
+ data.tar.gz: c5d9cb92a6785bc719b810d66ea18badfd201c2c3fdf9ae438a5e4c41fca6994
5
5
  SHA512:
6
- metadata.gz: e9bef6e3ecd74e07c89c15e374c414dca442d45947da635d97df64a716949f05fdd0b25b6a4ab6090e5f8f057bf3bd89b60498cca2e7d9cef2ef0be2146d42b1
7
- data.tar.gz: d067ebfeb63297953a98452856d853771d2c4d8e0589b00643a007cc70a5ef5b28ccbc4c9a3f1a289113710b0bb18a6e1471e31152d1ec94be17ba1bf80a76b8
6
+ metadata.gz: e3488671822dfdd73075c1f6c891ec961e62815807313b1fec8d229a70a3fd8867378b7c0089f4e697e3ed17e5fe3e2b0c19bdaea00d214c7df5817a3506f07a
7
+ data.tar.gz: aa423b93528d9f84023bf40707739cc730c73eecf0a0e0ef45277ca68834a301e25a03195d8a8cd9f7b0da0bd8eff55012b8eefc5730b4f35c90d9456e3a18a4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.1.0] - 2026-05-15
4
+
5
+ - Add `dx doctor` diagnostics command with `dx check` alias.
6
+ - Add shell integration version marker in `~/.dx.sh` and doctor warning for stale shell script versions.
7
+ - Complete `CLI` modularization by extracting remaining command and support logic into command modules.
8
+
3
9
  ## [0.1.0] - 2026-05-01
4
10
 
5
11
  - Initial release
data/README.md CHANGED
@@ -2,19 +2,25 @@
2
2
 
3
3
  This repo contains `dx` (`dev_context`), a script for managing developer context across repository directories and branches.
4
4
 
5
+ > Note: this Ruby implementation now lives in `implementations/ruby` within the
6
+ > multi-language `dev_context` monorepo.
7
+
5
8
  A context is one `(repo_dir, branch)` pair. Context names can be explicit, or implicit as `<repo_basename>:<branch>` (stored lowercase), and resolved with exact then FSF-style fuzzy matching.
6
9
 
7
10
  ## Installation
8
11
 
9
12
  gem install -n ~/bin dev_context
10
13
 
11
- Install the gem in the current ruby gem path, with the executable installed as `~/bin/dev_context.rb`.
14
+ Install the gem in the current ruby gem path, with executables installed as:
15
+
16
+ - `~/bin/dev_context` (launcher, honors `DX_IMPL`)
17
+ - `~/bin/dev_context-ruby` (Ruby implementation entrypoint)
12
18
 
13
19
  To clone from the github repo:
14
20
 
15
21
  cd ~/src/github # or wherever you keep github repos
16
22
  git clone https://github.com/aks/dev_context.git
17
- cd dev_context
23
+ cd dev_context/implementations/ruby
18
24
  bundle install
19
25
  bundle exec rake install
20
26
 
@@ -36,16 +42,16 @@ To support `cd`, `dx` should be wrapped by a shell function that evaluates shell
36
42
  ```bash
37
43
  dx() {
38
44
  case "$1" in
39
- cd|activate|pushd|popd)
45
+ cd|activate|pushd|popd|pu|po)
40
46
  local out
41
- out="$(DX_SHELL_WRAPPED=1 command dev_context.rb "$@")" || return $?
47
+ out="$(DX_SHELL_WRAPPED=1 command dev_context "$@")" || return $?
42
48
  case "$out" in
43
49
  "# DX_SHELL_EVAL"*) eval "$out" ;;
44
50
  *) printf "%s\n" "$out" ;;
45
51
  esac
46
52
  ;;
47
53
  *)
48
- command dev_context.rb "$@"
54
+ command dev_context "$@"
49
55
  ;;
50
56
  esac
51
57
  }
@@ -81,8 +87,8 @@ dx create -v feature:abc-123 ~/src/github/dev_context feature/abc-123
81
87
 
82
88
  ### Context stack commands
83
89
 
84
- - `dx pushd CONTEXT|PATH|URL`: activate and push context to top of stack
85
- - `dx popd [CONTEXT]`: pop the top context (or a specific one); if a new top exists, switch CWD to it
90
+ - `dx pushd [CONTEXT|PATH|URL|+N|-N]`: activate/push context, or rotate indexed stack entry to top
91
+ - `dx popd [CONTEXT|+N|-N]`: pop selected entry; with no args, same as `dx popd +0`
86
92
 
87
93
  ### `DX_PATH` repo lookup
88
94
 
@@ -144,8 +150,8 @@ dx active
144
150
  dx activate CONTEXT|PATH|URL
145
151
  dx cd CONTEXT|PATH|URL
146
152
  dx deactivate CONTEXT
147
- dx pushd CONTEXT|PATH|URL
148
- dx popd [CONTEXT]
153
+ dx pushd [CONTEXT|PATH|URL|+N|-N]
154
+ dx popd [CONTEXT|+N|-N]
149
155
  dx status|wip [--all] [--dirty] [-b BRANCHPATTERN] [-p PATHPATTERN]
150
156
  dx repos [-b BRANCHPATTERN] [-p PATHPATTERN] [PATTERN]
151
157
  dx stashes [--list] [PATTERN]
data/bin/dev_context ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/dev_context"
5
+
6
+ selected_impl = ENV["DX_IMPL"].to_s.strip.downcase
7
+ selected_impl = "ruby" if selected_impl.empty?
8
+
9
+ if %w[--version -V version].include?(ARGV.first)
10
+ if selected_impl == "ruby"
11
+ exit DevContext::CLI.run(ARGV)
12
+ end
13
+
14
+ candidate = "dev_context-#{selected_impl}"
15
+ begin
16
+ exec(candidate, *ARGV)
17
+ rescue Errno::ENOENT
18
+ warn("dx: selected implementation '#{selected_impl}' is not installed on PATH")
19
+ warn("dx: expected executable: #{candidate}")
20
+ warn("dx: set DX_IMPL=ruby or install #{candidate}")
21
+ exit 127
22
+ end
23
+ end
24
+ if selected_impl == "ruby"
25
+ exit DevContext::CLI.run(ARGV)
26
+ end
27
+
28
+ candidate = "dev_context-#{selected_impl}"
29
+ begin
30
+ exec(candidate, *ARGV)
31
+ rescue Errno::ENOENT
32
+ warn("dx: selected implementation not found on PATH: #{candidate}")
33
+ warn("dx: set DX_IMPL=ruby or install #{candidate}")
34
+ exit 127
35
+ end
@@ -12,6 +12,7 @@ module DevContext
12
12
  include Commands::Support
13
13
  include Commands::ContextLifecycle
14
14
  include Commands::GitOps
15
+ include Commands::Doctor
15
16
  include Commands::SearchHelpers
16
17
  include Commands::Find
17
18
  include Commands::Repos
@@ -27,16 +28,17 @@ module DevContext
27
28
  COLOR_YELLOW = "\e[33m"
28
29
  COLOR_MAGENTA = "\e[35m"
29
30
 
30
- def self.run(argv, out: $stdout, err: $stderr, env: ENV, pwd: Dir.pwd)
31
- new(argv, out: out, err: err, env: env, pwd: pwd).run
31
+ def self.run(argv, out: $stdout, err: $stderr, env: ENV, pwd: Dir.pwd, stdin: STDIN)
32
+ new(argv, out: out, err: err, env: env, pwd: pwd, stdin: stdin).run
32
33
  end
33
34
 
34
- def initialize(argv, out:, err:, env:, pwd:)
35
+ def initialize(argv, out:, err:, env:, pwd:, stdin: STDIN)
35
36
  @argv = argv.dup
36
37
  @out = out
37
38
  @err = err
38
39
  @env = env
39
40
  @pwd = pwd
41
+ @stdin = stdin
40
42
  @config = Config.new
41
43
  @matcher = Matcher.new(config: @config)
42
44
  end
@@ -45,6 +47,7 @@ module DevContext
45
47
  raw_command = argv.shift
46
48
 
47
49
  return default_action if raw_command.nil?
50
+ raw_command = "version" if %w[--version -V].include?(raw_command)
48
51
 
49
52
  resolution = resolve_command(raw_command)
50
53
  command = resolution[:command]
@@ -62,23 +65,26 @@ module DevContext
62
65
  end
63
66
 
64
67
  case command
65
- when "init" then cmd_init
66
- when "help", "--help", "-h" then cmd_help
67
- when "add" then cmd_add
68
- when "clone" then cmd_clone
69
- when "create" then cmd_create
70
- when "remove" then cmd_remove
71
- when "active" then cmd_active
72
- when "deactivate" then cmd_deactivate
73
- when "find" then cmd_find
74
- when "stashes" then cmd_stashes
75
- when "repos" then cmd_repos
76
- when "status", "wip" then cmd_status(mode: command)
77
- when "diff" then cmd_diff
78
- when "branches", "br" then cmd_branches
79
- when "co", "checkout" then cmd_checkout
80
- when "cd", "activate", "pushd" then cmd_activate
81
- when "popd" then cmd_pop
68
+ when "version" then cmd_version
69
+ when "init" then cmd_init
70
+ when "help", "--help", "-h" then cmd_help
71
+ when "add" then cmd_add
72
+ when "clone" then cmd_clone
73
+ when "create" then cmd_create
74
+ when "remove" then cmd_remove
75
+ when "active" then cmd_active
76
+ when "deactivate" then cmd_deactivate
77
+ when "doctor", "check" then cmd_doctor
78
+ when "find" then cmd_find
79
+ when "stashes" then cmd_stashes
80
+ when "repos" then cmd_repos
81
+ when "status", "wip" then cmd_status(mode: command)
82
+ when "diff" then cmd_diff
83
+ when "branches", "br" then cmd_branches
84
+ when "co", "checkout" then cmd_checkout
85
+ when "pushd" then cmd_push
86
+ when "cd", "activate" then cmd_activate
87
+ when "popd" then cmd_pop
82
88
  else
83
89
  err.puts("dx: unknown command '#{raw_command}'")
84
90
  cmd_help
@@ -88,10 +94,10 @@ module DevContext
88
94
 
89
95
  private
90
96
 
91
- attr_reader :argv, :out, :err, :env, :pwd, :config, :matcher
97
+ attr_reader :argv, :out, :err, :env, :pwd, :stdin, :config, :matcher
92
98
 
93
99
  COMMANDS = %w[
94
- init help add clone create remove active deactivate find repos stashes status wip diff branches br co checkout cd activate pushd popd
100
+ version init help add clone create remove active deactivate doctor check find repos stashes status wip diff branches br co checkout cd activate pushd popd
95
101
  ].freeze
96
102
 
97
103
  def default_action
@@ -99,5 +105,10 @@ module DevContext
99
105
 
100
106
  cmd_status
101
107
  end
108
+
109
+ def cmd_version
110
+ out.puts("dx #{DevContext::VERSION} (impl: ruby)")
111
+ 0
112
+ end
102
113
  end
103
114
  end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+ require "open3"
3
+
4
+ module DevContext
5
+ module Commands
6
+ module Doctor
7
+ def cmd_doctor
8
+ return usage_error("dx doctor") unless argv.empty?
9
+
10
+ checks = []
11
+ checks.concat(shell_integration_checks)
12
+ checks.concat(config_structure_checks)
13
+ checks.concat(repo_integrity_checks)
14
+ checks.concat(environment_checks)
15
+
16
+ label_w = [checks.map { |c| c[:label].length }.max || 0, "Check".length].max
17
+ state_w = [checks.map { |c| c[:state].length }.max || 0, "State".length].max
18
+
19
+ out.puts(format("%-#{state_w}s %-#{label_w}s %s", "State", "Check", "Message"))
20
+ out.puts(format("%-#{state_w}s %-#{label_w}s %s", "-" * state_w, "-" * label_w, "-" * 7))
21
+ checks.each do |check|
22
+ out.puts(format("%-#{state_w}s %-#{label_w}s %s", check[:state], check[:label], check[:message]))
23
+ end
24
+
25
+ failures = checks.count { |c| c[:state] == "FAIL" }
26
+ warnings = checks.count { |c| c[:state] == "WARN" }
27
+ out.puts
28
+ out.puts("doctor summary: #{checks.length} checks, #{failures} failures, #{warnings} warnings")
29
+
30
+ failures.zero? ? 0 : 1
31
+ end
32
+
33
+ private
34
+
35
+ def shell_integration_checks
36
+ checks = []
37
+ shell_path = env.fetch("DX_SHELL_PATH", ShellSetup::DEFAULT_PATH)
38
+ if File.exist?(shell_path)
39
+ content = File.read(shell_path)
40
+ if content.include?(ShellSetup::MANAGED_MARKER)
41
+ checks << ok_check("shell-script", "Found managed shell integration at #{shell_path}")
42
+ checks << ok_check("shell-managed", "Shell integration is managed by dx init")
43
+ shell_version = extract_shell_script_version(content)
44
+ if shell_version.nil?
45
+ checks << warn_check("shell-version", "Managed shell integration is missing version marker")
46
+ elsif shell_version == DevContext::SHELL_VERSION
47
+ checks << ok_check("shell-version", "Shell integration matches shell version #{DevContext::SHELL_VERSION}")
48
+ else
49
+ checks << warn_check("shell-version", "Shell integration is #{shell_version}; current shell version is #{DevContext::SHELL_VERSION} (run `dx init`)")
50
+ end
51
+ else
52
+ checks << warn_check("shell-script", "Shell file exists but is unmanaged: #{shell_path}")
53
+ end
54
+ else
55
+ checks << fail_check("shell-script", "Shell integration file missing: #{shell_path} (run `dx init`)")
56
+ end
57
+ checks
58
+ end
59
+
60
+ def config_structure_checks
61
+ checks = []
62
+ if config.initialized?
63
+ checks << ok_check("config-file", "Config file exists at #{config.path}")
64
+ else
65
+ checks << fail_check("config-file", "Config file missing at #{config.path} (run `dx init`)")
66
+ return checks
67
+ end
68
+
69
+ active_missing = config.active_stack.reject { |name| config.contexts.key?(name) }
70
+ if active_missing.empty?
71
+ checks << ok_check("active-stack", "Active stack entries map to known contexts")
72
+ else
73
+ checks << fail_check("active-stack", "Active stack has missing contexts: #{active_missing.join(', ')}")
74
+ end
75
+ checks
76
+ end
77
+
78
+ def repo_integrity_checks
79
+ paths = config.repos.values.map { |repo| repo["path"] }.compact.uniq
80
+ return [warn_check("known-repos", "No known repos in config")] if paths.empty?
81
+
82
+ missing = []
83
+ non_git = []
84
+ paths.each do |path|
85
+ expanded = File.expand_path(path)
86
+ unless File.directory?(expanded)
87
+ missing << expanded
88
+ next
89
+ end
90
+ non_git << expanded unless git_repo?(expanded)
91
+ end
92
+
93
+ checks = []
94
+ if missing.empty?
95
+ checks << ok_check("repo-paths", "All known repo paths exist")
96
+ else
97
+ checks << fail_check("repo-paths", "Missing repo paths: #{missing.join(', ')}")
98
+ end
99
+
100
+ if non_git.empty?
101
+ checks << ok_check("git-repos", "All existing known repos are git repos")
102
+ else
103
+ checks << warn_check("git-repos", "Non-git known paths: #{non_git.join(', ')}")
104
+ end
105
+ checks
106
+ end
107
+
108
+ def environment_checks
109
+ checks = []
110
+
111
+ found_exec = command_success?("sh", "-lc", "command -v dev_context >/dev/null 2>&1")
112
+ if found_exec
113
+ checks << ok_check("executable", "`dev_context` is on PATH")
114
+ if command_success?("dev_context", "help")
115
+ checks << ok_check("executable-run", "`dev_context help` executes successfully")
116
+ else
117
+ checks << warn_check("executable-run", "`dev_context` found but failed to execute `help`")
118
+ end
119
+ else
120
+ checks << warn_check("executable", "`dev_context` not found on PATH")
121
+ end
122
+
123
+ dx_path_raw = env["DX_PATH"]
124
+ if dx_path_raw.to_s.strip.empty?
125
+ checks << warn_check("DX_PATH", "DX_PATH is not set")
126
+ else
127
+ roots = dx_path_raw.split(/[:;]/).flat_map { |entry| Dir.glob(File.expand_path(entry.to_s.strip)) }.uniq
128
+ dirs = roots.select { |entry| File.directory?(entry) }
129
+ if dirs.empty?
130
+ checks << warn_check("DX_PATH", "No valid directories resolved from DX_PATH")
131
+ else
132
+ checks << ok_check("DX_PATH", "Resolved #{dirs.length} directory root(s)")
133
+ end
134
+ end
135
+
136
+ checks
137
+ end
138
+
139
+ def ok_check(label, message)
140
+ { state: "OK", label: label, message: message }
141
+ end
142
+
143
+ def warn_check(label, message)
144
+ { state: "WARN", label: label, message: message }
145
+ end
146
+
147
+ def fail_check(label, message)
148
+ { state: "FAIL", label: label, message: message }
149
+ end
150
+
151
+ def extract_shell_script_version(content)
152
+ escaped = Regexp.escape(ShellSetup::VERSION_MARKER_PREFIX)
153
+ match = content.match(/^\s*#\s*#{escaped}\s+(.+?)\s*$/)
154
+ return nil unless match
155
+
156
+ match[1]
157
+ end
158
+
159
+ def command_success?(*cmd)
160
+ _out, status = Open3.capture2e(env.to_h, *cmd)
161
+ status.success?
162
+ rescue StandardError
163
+ false
164
+ end
165
+ end
166
+ end
167
+ end
@@ -29,8 +29,34 @@ module DevContext
29
29
  0
30
30
  end
31
31
 
32
+ def cmd_push
33
+ target = argv.shift
34
+ return usage_error("dx pushd [CONTEXT|+N|-N]") unless argv.empty?
35
+
36
+ if target&.match?(/\A[+-]\d+\z/)
37
+ return rotate_active_stack_to(target)
38
+ end
39
+
40
+ return usage_error("dx pushd [CONTEXT|+N|-N]") if blank?(target)
41
+
42
+ context, code = resolve_or_create_context(target)
43
+ return code unless code.zero?
44
+
45
+ config.activate_context!(context.fetch("name"))
46
+ script = ShellEmitter.new(
47
+ context: context,
48
+ remote_name: env.fetch("DX_GIT_REMOTE_NAME", "USE-REPO"),
49
+ auto_create_local_branch: truthy?(env.fetch("DX_AUTO_CREATE_LOCAL_BRANCH", "true"))
50
+ ).activation_script
51
+ out.write(script)
52
+ 0
53
+ end
54
+
32
55
  def cmd_pop
33
56
  target = argv.shift
57
+ return usage_error("dx popd [CONTEXT|+N|-N]") unless argv.empty?
58
+ return pop_by_stack_index("+0") if target.nil?
59
+ return pop_by_stack_index(target) if target.match?(/\A[+-]\d+\z/)
34
60
 
35
61
  if target
36
62
  context = matcher.resolve(target)
@@ -158,6 +184,72 @@ module DevContext
158
184
  out.puts("Checked out #{feature} in #{context_name}")
159
185
  0
160
186
  end
187
+
188
+ private
189
+
190
+ def rotate_active_stack_to(token)
191
+ stack = config.active_stack
192
+ return no_active_contexts if stack.empty?
193
+
194
+ idx = parse_stack_index_token(token, stack.length)
195
+ if idx.nil?
196
+ err.puts("dx: invalid stack index '#{token}'")
197
+ return 2
198
+ end
199
+
200
+ context_name = stack[idx]
201
+ context = config.contexts[context_name]
202
+ unless context
203
+ err.puts("dx: active stack entry is missing context: #{context_name}")
204
+ return 1
205
+ end
206
+
207
+ config.activate_context!(context_name)
208
+ script = ShellEmitter.new(
209
+ context: context,
210
+ remote_name: env.fetch("DX_GIT_REMOTE_NAME", "USE-REPO"),
211
+ auto_create_local_branch: truthy?(env.fetch("DX_AUTO_CREATE_LOCAL_BRANCH", "true"))
212
+ ).activation_script
213
+ out.write(script)
214
+ 0
215
+ end
216
+
217
+ def pop_by_stack_index(token)
218
+ stack = config.active_stack
219
+ return no_active_contexts if stack.empty?
220
+
221
+ idx = parse_stack_index_token(token, stack.length)
222
+ if idx.nil?
223
+ err.puts("dx: invalid stack index '#{token}'")
224
+ return 2
225
+ end
226
+
227
+ context_name = stack.delete_at(idx)
228
+ context = config.contexts[context_name]
229
+ config.send(:save!)
230
+ out.puts("Popped #{context ? context.fetch('name') : context_name}")
231
+
232
+ new_top = config.active_contexts.first
233
+ return 0 unless new_top
234
+
235
+ out.puts("cd #{Shellwords.escape(new_top.fetch('repo_path'))}")
236
+ 0
237
+ end
238
+
239
+ def parse_stack_index_token(token, stack_size)
240
+ return nil unless token.match?(/\A[+-]\d+\z/)
241
+
242
+ n = token[1..].to_i
243
+ idx = token.start_with?("+") ? n : (stack_size - 1 - n)
244
+ return nil if idx.negative? || idx >= stack_size
245
+
246
+ idx
247
+ end
248
+
249
+ def no_active_contexts
250
+ out.puts("No active contexts")
251
+ 0
252
+ end
161
253
  end
162
254
  end
163
255
  end
@@ -25,15 +25,17 @@ module DevContext
25
25
  dx checkout|co [-b] FEATURE [CONTEXT]
26
26
  dx clone URL [PATH]
27
27
  dx create NAME [PATH] [BRANCH]
28
+ dx doctor|check
28
29
  dx deactivate CONTEXT
29
30
  dx diff [CONTEXT]
30
31
  dx find [--stashes|--branches|--paths] <pattern>
31
32
  dx find --all
32
33
  dx <pattern> # shorthand for: dx find <pattern>
33
34
  dx help
34
- dx init
35
- dx popd [CONTEXT]
36
- dx pushd CONTEXT|PATH|URL
35
+ dx init [ruby|go|elixir]
36
+ dx version
37
+ dx popd [CONTEXT|+N|-N]
38
+ dx pushd [CONTEXT|PATH|URL|+N|-N]
37
39
  dx repos [PATTERN]
38
40
  dx remove CONTEXT|PATH
39
41
  dx stashes [--list] [PATTERN]
@@ -52,16 +54,24 @@ module DevContext
52
54
  "Usage: dx stashes [--list] [PATTERN]\n\nShow repos with stashes. Provide PATTERN to filter stash titles; use --list to print matching entries."
53
55
  when "find"
54
56
  "Usage: dx find [--stashes|--branches|--paths] <pattern>\n dx find --all\n\nSearch stash titles, branches, and repo paths. Use --all to show likely-forgotten work."
57
+ when "doctor", "check"
58
+ "Usage: dx doctor\n dx check\n\nRun environment and config diagnostics."
59
+ when "init"
60
+ "Usage: dx init [ruby|go|elixir]\n\nInitialize dx files. If IMPL is given, set DX_IMPL non-interactively."
61
+ when "version"
62
+ "Usage: dx version\n dx --version\n dx -V\n\nPrint the active dx implementation and version."
55
63
  when "add"
56
64
  "Usage: dx add [-c NAME] [-n] [-v] PATH|URL ..."
57
65
  when "clone"
58
66
  "Usage: dx clone [-c NAME] [-n] [-v] URL [PATH]"
59
67
  when "create"
60
68
  "Usage: dx create [-c NAME] [-n] [-v] NAME [PATH] [BRANCH]"
61
- when "activate", "cd", "pushd"
69
+ when "activate", "cd"
62
70
  "Usage: dx #{command} CONTEXT|PATH|URL"
71
+ when "pushd"
72
+ "Usage: dx pushd [CONTEXT|PATH|URL|+N|-N]\n\nCONTEXT accepts exact or fuzzy-matched context names. PATH and URL resolve/create contexts. +N/-N select existing active-stack entries (+0 top/left, -0 bottom/right) and rotate that entry to top."
63
73
  when "popd"
64
- "Usage: dx popd [CONTEXT]"
74
+ "Usage: dx popd [CONTEXT|+N|-N]\n\nWith no args, same as `dx popd +0`. CONTEXT accepts exact or fuzzy-matched context names. +N counts from top/left (+0 top), -N from bottom/right (-0 bottom)."
65
75
  when "remove"
66
76
  "Usage: dx remove CONTEXT|PATH"
67
77
  when "diff"
@@ -4,6 +4,13 @@ module DevContext
4
4
  module Commands
5
5
  module Init
6
6
  def cmd_init
7
+ explicit_impl = argv.shift
8
+ return usage_error("dx init [ruby|go|elixir]") unless argv.empty?
9
+ if explicit_impl && !valid_impl?(explicit_impl)
10
+ err.puts("dx: unknown implementation '#{explicit_impl}'")
11
+ return usage_error("dx init [ruby|go|elixir]")
12
+ end
13
+
7
14
  created = config.init!
8
15
  shell_status = ShellSetup.new.install!
9
16
 
@@ -15,8 +22,61 @@ module DevContext
15
22
  end
16
23
  out.puts(shell_message)
17
24
  out.puts('Add `source ~/.dx.sh` to ~/.zshrc or ~/.bashrc')
25
+ impl = if explicit_impl
26
+ explicit_impl.downcase
27
+ elsif stdin.tty?
28
+ prompt_dx_impl
29
+ end
30
+ if impl
31
+ updated_files = persist_dx_impl_choice(impl)
32
+ out.puts("Set DX_IMPL=#{impl} in #{updated_files.join(', ')}")
33
+ end
18
34
  0
19
35
  end
36
+
37
+ private
38
+
39
+ def prompt_dx_impl
40
+ out.print("Choose DX implementation [ruby/go/elixir] (default: ruby): ")
41
+ raw = stdin.gets.to_s.strip.downcase
42
+ return "ruby" if raw.empty?
43
+ return raw if valid_impl?(raw)
44
+
45
+ err.puts("dx: unknown implementation '#{raw}', defaulting to ruby")
46
+ "ruby"
47
+ end
48
+
49
+ def valid_impl?(value)
50
+ %w[ruby go elixir].include?(value.to_s.strip.downcase)
51
+ end
52
+
53
+ def persist_dx_impl_choice(choice)
54
+ home = env.fetch("HOME", File.expand_path("~"))
55
+ zshrc = File.join(home, ".zshrc")
56
+ bashrc = File.join(home, ".bashrc")
57
+ preferred = env.fetch("SHELL", "").include?("bash") ? bashrc : zshrc
58
+ existing = [zshrc, bashrc].select { |path| File.exist?(path) }
59
+ targets = existing.empty? ? [preferred] : existing
60
+
61
+ targets.each { |path| write_dx_impl_to_rc(path, choice) }
62
+ targets
63
+ end
64
+
65
+ def write_dx_impl_to_rc(path, choice)
66
+ line = "export DX_IMPL=#{choice}"
67
+ current = File.exist?(path) ? File.read(path) : ""
68
+ updated =
69
+ if current.match?(/^\s*(?:export\s+)?DX_IMPL=.*$/)
70
+ current.gsub(/^\s*(?:export\s+)?DX_IMPL=.*$/, line)
71
+ elsif current.empty?
72
+ "#{line}\n"
73
+ elsif current.end_with?("\n")
74
+ "#{current}#{line}\n"
75
+ else
76
+ "#{current}\n#{line}\n"
77
+ end
78
+ File.write(path, updated)
79
+ end
20
80
  end
21
81
  end
22
82
  end
@@ -280,8 +280,9 @@ module DevContext
280
280
  end
281
281
 
282
282
  def render_status_table(rows)
283
- display_rows = rows.map do |row|
284
- row.merge(path: display_path(row[:path]))
283
+ display_rows = rows.each_with_index.map do |row, idx|
284
+ indexed_path = "[#{idx}] #{display_path(row[:path])}"
285
+ row.merge(path: indexed_path)
285
286
  end
286
287
 
287
288
  path_w = [display_rows.map { |r| r[:path].length }.max || 0, "Path".length].max
@@ -4,6 +4,7 @@ module DevContext
4
4
  class ShellSetup
5
5
  DEFAULT_PATH = File.expand_path("~/.dx.sh").freeze
6
6
  MANAGED_MARKER = "DevContext shell integration generated by `dx init`".freeze
7
+ VERSION_MARKER_PREFIX = "DX Shell Version:".freeze
7
8
 
8
9
  attr_reader :path
9
10
 
@@ -36,20 +37,33 @@ module DevContext
36
37
  <<~SH
37
38
  #!/usr/bin/env sh
38
39
  # #{MANAGED_MARKER}
40
+ # #{VERSION_MARKER_PREFIX} #{DevContext::SHELL_VERSION}
39
41
  # shellcheck shell=sh
40
42
 
41
43
  dx() {
44
+ local bin
45
+ case "${DX_IMPL:-go}" in
46
+ go)
47
+ bin=dev_context-go ;;
48
+ ruby)
49
+ bin=dev_context.rb ;;
50
+ elixir)
51
+ bin=dev_context-ex ;;
52
+ *)
53
+ echo 1>&2 "No idea what $DX_IMPL means!" ; exit 1 ;;
54
+ esac
55
+ local out
42
56
  case "$1" in
43
57
  cd|activate|pushd|popd)
44
58
  local out
45
- out="$(DX_SHELL_WRAPPED=1 command dev_context.rb "$@")" || return $?
59
+ out="$(DX_SHELL_WRAPPED=1 command $bin "$@")" || return $?
46
60
  case "$out" in
47
61
  "# DX_SHELL_EVAL"*) eval "$out" ;;
48
62
  *) printf "%s\\n" "$out" ;;
49
63
  esac
50
64
  ;;
51
65
  *)
52
- command dev_context.rb "$@"
66
+ command $bin "$@"
53
67
  ;;
54
68
  esac
55
69
  }
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DevContext
4
+ SHELL_VERSION = "1"
4
5
  RELEASES = [
5
- ["1.0.1", '2026-05-12']
6
+ ["1.0.1", '2026-05-12'],
7
+ ["1.1.0", '2026-05-15'],
8
+ ["1.2.1", '2026-05-26']
6
9
  ].freeze
7
10
  VERSION = RELEASES.last.first
8
11
  end
data/lib/dev_context.rb CHANGED
@@ -8,6 +8,7 @@ require_relative "dev_context/commands/help"
8
8
  require_relative "dev_context/commands/support"
9
9
  require_relative "dev_context/commands/context_lifecycle"
10
10
  require_relative "dev_context/commands/git_ops"
11
+ require_relative "dev_context/commands/doctor"
11
12
  require_relative "dev_context/commands/find"
12
13
  require_relative "dev_context/commands/search_helpers"
13
14
  require_relative "dev_context/commands/repos"
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dev_context
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alan Stebbens
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-05-26 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: fuubar
@@ -53,12 +54,13 @@ dependencies:
53
54
  version: '3.0'
54
55
  description: |
55
56
  DevContext (aka `dx`) is a developer context manager that unifies directory-level context
56
- switching, multi-repo awareness, and Git-aware introspection. It helps you manage multiple
57
- active projects with an activation stack.
57
+ switching, branch awareness, multi-repo awareness, and Git-aware introspection. It helps
58
+ you manage multiple active projects with an active stack.
58
59
  email:
59
60
  - aks@stebbens.org
60
61
  executables:
61
- - dev_context.rb
62
+ - dev_context
63
+ - dev_context-ruby
62
64
  extensions: []
63
65
  extra_rdoc_files: []
64
66
  files:
@@ -68,11 +70,13 @@ files:
68
70
  - LICENSE.txt
69
71
  - README.md
70
72
  - bin/console
71
- - bin/dev_context.rb
73
+ - bin/dev_context
74
+ - bin/dev_context-ruby
72
75
  - bin/setup
73
76
  - lib/dev_context.rb
74
77
  - lib/dev_context/cli.rb
75
78
  - lib/dev_context/commands/context_lifecycle.rb
79
+ - lib/dev_context/commands/doctor.rb
76
80
  - lib/dev_context/commands/find.rb
77
81
  - lib/dev_context/commands/git_ops.rb
78
82
  - lib/dev_context/commands/help.rb
@@ -96,6 +100,7 @@ metadata:
96
100
  allowed_push_host: https://rubygems.org
97
101
  source_code_uri: https://github.com/aks/dev_context
98
102
  changelog_uri: https://github.com/aks/dev_context/blob/main/CHANGELOG.md
103
+ post_install_message:
99
104
  rdoc_options: []
100
105
  require_paths:
101
106
  - lib
@@ -110,7 +115,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
110
115
  - !ruby/object:Gem::Version
111
116
  version: '0'
112
117
  requirements: []
113
- rubygems_version: 4.0.11
118
+ rubygems_version: 3.5.22
119
+ signing_key:
114
120
  specification_version: 4
115
121
  summary: A developer context manager for multi-repo workflows
116
122
  test_files: []
File without changes