dev_context 1.0.1 → 2.0.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: a909bb66276763e6e8cf92b56a703f63a7a81637977dd4e2c2ac6a2495ac6a0f
4
+ data.tar.gz: dbf0c96d8324235871af7ffda020f761287d66c980d7d34ae58082c7018ebffa
5
5
  SHA512:
6
- metadata.gz: e9bef6e3ecd74e07c89c15e374c414dca442d45947da635d97df64a716949f05fdd0b25b6a4ab6090e5f8f057bf3bd89b60498cca2e7d9cef2ef0be2146d42b1
7
- data.tar.gz: d067ebfeb63297953a98452856d853771d2c4d8e0589b00643a007cc70a5ef5b28ccbc4c9a3f1a289113710b0bb18a6e1471e31152d1ec94be17ba1bf80a76b8
6
+ metadata.gz: d6169e888f0c0d6ec0ad01828ccbf24f1e961616f7ff01d044c63b9bef40cccc03b131a6b77ddae3ad05161c0a6a105545d555ceeec3659c1c3ec43bbcbdb903
7
+ data.tar.gz: 45842d4f6b81a31a2323683313bf11772318e62c45a2ef5fa9df4fa80809cc8e83dfe3f167b4bfe036ae8fe48a6bd7768d89284206698bdd2d767de703055558
data/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ### Monorepo tooling (root-level)
4
+
5
+ - Add `CLAUDE.md` with codebase guidance for Claude Code.
6
+ - Add root `Gemfile` proxying to `implementations/ruby/` via `eval_gemfile` so `bundle install` works from repo root.
7
+ - Add root `.rspec` and `spec/` symlinks so `bundle exec rspec` works from repo root without `cd`.
8
+ - Add `ruby-bundle-ready` Makefile prerequisite so `test-ruby`, `install-ruby`, and `release-ruby` ensure Bundler state before running.
9
+
10
+ ## [2.0.1] - 2026-06-25
11
+
12
+ - cleaned up erb and updated psych gems to avoid ARM crash on an older version of psych.
13
+
14
+ ## [2.0.0] - 2026-06-23
15
+
16
+ ### Added
17
+
18
+ - Add `dx import` to ingest shell dirstack into `dx` active stack and resynchronize shell stack via eval script.
19
+ - Add optional auto-reconcile gate via `DX_AUTO_IMPORT_SHELL_STACK=true` for stateful commands (`activate|cd`, `pushd`, `popd`).
20
+ - Add shell wrapper transport of live dirstack via `DX_SHELL_STACK` for wrapped stack-aware commands.
21
+ - Add end-to-end coverage in `bash` and `zsh` for mixed shell/dx dirstack mutations and reconciliation.
22
+
23
+ ### Changed
24
+
25
+ - Strengthen `dx`/shell stack interoperability so manual shell `pushd`/`popd` changes can be reconciled by `dx import`.
26
+ - Extend wrapped command handling in shell integration to include `import`.
27
+
28
+ ### Breaking Changes
29
+
30
+ - Shell integration semantics now include dirstack payload transport (`DX_SHELL_STACK`) and `import` in wrapped eval paths.
31
+ - Users relying on previous loose coupling between shell dirstack and `dx` active stack should treat this release as a behavior-boundary change.
32
+
33
+ ### Migration Notes
34
+
35
+ - Run `dx init` and re-source `~/.dx.sh` after upgrading.
36
+ - Use `dx import` to reconcile shell stack changes into `dx` state on demand.
37
+ - Enable `DX_AUTO_IMPORT_SHELL_STACK=true` only if you want reconciliation to happen automatically before stateful `dx` commands.
38
+
39
+ ## [1.1.0] - 2026-05-15
40
+
41
+ - Add `dx doctor` diagnostics command with `dx check` alias.
42
+ - Add shell integration version marker in `~/.dx.sh` and doctor warning for stale shell script versions.
43
+ - Complete `CLI` modularization by extracting remaining command and support logic into command modules.
44
+
3
45
  ## [0.1.0] - 2026-05-01
4
46
 
5
47
  - Initial release
data/README.md CHANGED
@@ -2,19 +2,26 @@
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-rb` (Ruby implementation entrypoint)
18
+ - `~/bin/dev_context-ruby` (legacy Ruby implementation alias)
12
19
 
13
20
  To clone from the github repo:
14
21
 
15
22
  cd ~/src/github # or wherever you keep github repos
16
23
  git clone https://github.com/aks/dev_context.git
17
- cd dev_context
24
+ cd dev_context/implementations/ruby
18
25
  bundle install
19
26
  bundle exec rake install
20
27
 
@@ -35,17 +42,28 @@ To support `cd`, `dx` should be wrapped by a shell function that evaluates shell
35
42
 
36
43
  ```bash
37
44
  dx() {
45
+ local bin
46
+ case "${DX_IMPL:-go}" in
47
+ go)
48
+ bin=dev_context-go ;;
49
+ ruby)
50
+ bin=dev_context-rb ;;
51
+ elixir)
52
+ bin=dev_context-ex ;;
53
+ *)
54
+ echo 1>&2 "No idea what $DX_IMPL means!" ; exit 1 ;;
55
+ esac
38
56
  case "$1" in
39
- cd|activate|pushd|popd)
57
+ cd|activate|pushd|popd|pu|po)
40
58
  local out
41
- out="$(DX_SHELL_WRAPPED=1 command dev_context.rb "$@")" || return $?
59
+ out="$(DX_SHELL_WRAPPED=1 command $bin "$@")" || return $?
42
60
  case "$out" in
43
61
  "# DX_SHELL_EVAL"*) eval "$out" ;;
44
62
  *) printf "%s\n" "$out" ;;
45
63
  esac
46
64
  ;;
47
65
  *)
48
- command dev_context.rb "$@"
66
+ command $bin "$@"
49
67
  ;;
50
68
  esac
51
69
  }
@@ -81,8 +99,8 @@ dx create -v feature:abc-123 ~/src/github/dev_context feature/abc-123
81
99
 
82
100
  ### Context stack commands
83
101
 
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
102
+ - `dx pushd [CONTEXT|PATH|URL|+N|-N]`: activate/push context, or rotate indexed stack entry to top
103
+ - `dx popd [CONTEXT|+N|-N]`: pop selected entry; with no args, same as `dx popd +0`
86
104
 
87
105
  ### `DX_PATH` repo lookup
88
106
 
@@ -144,8 +162,8 @@ dx active
144
162
  dx activate CONTEXT|PATH|URL
145
163
  dx cd CONTEXT|PATH|URL
146
164
  dx deactivate CONTEXT
147
- dx pushd CONTEXT|PATH|URL
148
- dx popd [CONTEXT]
165
+ dx pushd [CONTEXT|PATH|URL|+N|-N]
166
+ dx popd [CONTEXT|+N|-N]
149
167
  dx status|wip [--all] [--dirty] [-b BRANCHPATTERN] [-p PATHPATTERN]
150
168
  dx repos [-b BRANCHPATTERN] [-p PATHPATTERN] [PATTERN]
151
169
  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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/dev_context"
5
+
6
+ exit DevContext::CLI.run(ARGV)
@@ -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]
@@ -61,24 +64,31 @@ module DevContext
61
64
  return cmd_help_topic(command)
62
65
  end
63
66
 
67
+ maybe_auto_import_shell_stack!(command)
68
+
64
69
  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
70
+ when "version" then cmd_version
71
+ when "init" then cmd_init
72
+ when "help", "--help", "-h" then cmd_help
73
+ when "add" then cmd_add
74
+ when "clone" then cmd_clone
75
+ when "create" then cmd_create
76
+ when "remove" then cmd_remove
77
+ when "active" then cmd_active
78
+ when "deactivate" then cmd_deactivate
79
+ when "doctor", "check" then cmd_doctor
80
+ when "find" then cmd_find
81
+ when "import" then cmd_import
82
+ when "stash" then cmd_stash
83
+ when "stashes" then cmd_stashes
84
+ when "repos" then cmd_repos
85
+ when "status", "wip" then cmd_status(mode: command)
86
+ when "diff" then cmd_diff
87
+ when "branches", "br" then cmd_branches
88
+ when "co", "checkout" then cmd_checkout
89
+ when "pushd" then cmd_push
90
+ when "cd", "activate" then cmd_activate
91
+ when "popd" then cmd_pop
82
92
  else
83
93
  err.puts("dx: unknown command '#{raw_command}'")
84
94
  cmd_help
@@ -88,16 +98,31 @@ module DevContext
88
98
 
89
99
  private
90
100
 
91
- attr_reader :argv, :out, :err, :env, :pwd, :config, :matcher
101
+ attr_reader :argv, :out, :err, :env, :pwd, :stdin, :config, :matcher
92
102
 
93
103
  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
104
+ version init help add clone create remove active deactivate doctor check find repos stash stashes status wip diff branches br co checkout cd activate pushd popd import
95
105
  ].freeze
96
106
 
107
+ STATEFUL_COMMANDS = %w[activate cd pushd popd].freeze
108
+
97
109
  def default_action
98
110
  return cmd_init unless config.initialized?
99
111
 
100
112
  cmd_status
101
113
  end
114
+
115
+ def cmd_version
116
+ out.puts("dx #{DevContext::VERSION} (impl: ruby)")
117
+ 0
118
+ end
119
+
120
+ def maybe_auto_import_shell_stack!(command)
121
+ return unless STATEFUL_COMMANDS.include?(command)
122
+ return unless truthy?(env.fetch("DX_AUTO_IMPORT_SHELL_STACK", "false"))
123
+ return unless shell_stack_imbalanced?
124
+
125
+ cmd_import(emit_script: false)
126
+ end
102
127
  end
103
128
  end
@@ -70,7 +70,8 @@ module DevContext
70
70
  script = ShellEmitter.new(
71
71
  context: context,
72
72
  remote_name: env.fetch("DX_GIT_REMOTE_NAME", "USE-REPO"),
73
- auto_create_local_branch: truthy?(env.fetch("DX_AUTO_CREATE_LOCAL_BRANCH", "true"))
73
+ auto_create_local_branch: truthy?(env.fetch("DX_AUTO_CREATE_LOCAL_BRANCH", "true")),
74
+ stack_paths: config.active_contexts.map { |ctx| ctx.fetch("repo_path") }
74
75
  ).activation_script
75
76
  out.write(script)
76
77
  0
@@ -0,0 +1,173 @@
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
+ shell_version = extract_shell_script_version(content)
43
+ if shell_version.nil?
44
+ checks << warn_check("shell-version", "Managed shell integration is missing version marker")
45
+ elsif shell_version == DevContext::SHELL_VERSION
46
+ checks << ok_check("shell-version", "Shell integration matches shell version #{DevContext::SHELL_VERSION}")
47
+ else
48
+ checks << warn_check("shell-version", "Shell integration is #{shell_version}; current shell version is #{DevContext::SHELL_VERSION} (run `dx init`)")
49
+ end
50
+ else
51
+ checks << warn_check("shell-script", "Shell file exists but is unmanaged: #{shell_path}")
52
+ end
53
+ else
54
+ checks << fail_check("shell-script", "Shell integration file missing: #{shell_path} (run `dx init`)")
55
+ end
56
+ checks
57
+ end
58
+
59
+ def config_structure_checks
60
+ checks = []
61
+ if config.initialized?
62
+ checks << ok_check("config-file", "Config file exists at #{config.path}")
63
+ else
64
+ checks << fail_check("config-file", "Config file missing at #{config.path} (run `dx init`)")
65
+ return checks
66
+ end
67
+
68
+ active_missing = config.active_stack.reject { |name| config.contexts.key?(name) }
69
+ if active_missing.empty?
70
+ checks << ok_check("active-stack", "Active stack entries map to known contexts")
71
+ else
72
+ checks << fail_check("active-stack", "Active stack has missing contexts: #{active_missing.join(', ')}")
73
+ end
74
+ checks
75
+ end
76
+
77
+ def repo_integrity_checks
78
+ paths = config.repos.values.map { |repo| repo["path"] }.compact.uniq
79
+ return [warn_check("known-repos", "No known repos in config")] if paths.empty?
80
+
81
+ missing = []
82
+ non_git = []
83
+ paths.each do |path|
84
+ expanded = File.expand_path(path)
85
+ unless File.directory?(expanded)
86
+ missing << expanded
87
+ next
88
+ end
89
+ non_git << expanded unless git_repo?(expanded)
90
+ end
91
+
92
+ checks = []
93
+ if missing.empty?
94
+ checks << ok_check("repo-paths", "All known repo paths exist")
95
+ elsif fixture_mode?
96
+ checks << warn_check("repo-paths", "Fixture mode enabled; ignoring missing repo paths: #{missing.join(', ')}")
97
+ else
98
+ checks << fail_check("repo-paths", "Missing repo paths: #{missing.join(', ')}")
99
+ end
100
+
101
+ if non_git.empty?
102
+ checks << ok_check("git-repos", "All existing known repos are git repos")
103
+ else
104
+ checks << warn_check("git-repos", "Non-git known paths: #{non_git.join(', ')}")
105
+ end
106
+ checks
107
+ end
108
+
109
+ def environment_checks
110
+ checks = []
111
+
112
+ found_exec = command_success?("sh", "-lc", "command -v dev_context >/dev/null 2>&1")
113
+ if found_exec
114
+ checks << ok_check("executable", "`dev_context` is on PATH")
115
+ if command_success?("dev_context", "help")
116
+ checks << ok_check("executable-run", "`dev_context help` executes successfully")
117
+ else
118
+ checks << warn_check("executable-run", "`dev_context` found but failed to execute `help`")
119
+ end
120
+ else
121
+ checks << warn_check("executable", "`dev_context` not found on PATH")
122
+ end
123
+
124
+ dx_path_raw = env["DX_PATH"]
125
+ if dx_path_raw.to_s.strip.empty?
126
+ checks << warn_check("DX_PATH", "DX_PATH is not set")
127
+ else
128
+ roots = dx_path_raw.split(/[:;]/).flat_map { |entry| Dir.glob(File.expand_path(entry.to_s.strip)) }.uniq
129
+ dirs = roots.select { |entry| File.directory?(entry) }
130
+ if dirs.empty?
131
+ checks << warn_check("DX_PATH", "No valid directories resolved from DX_PATH")
132
+ else
133
+ checks << ok_check("DX_PATH", "Resolved #{dirs.length} directory root(s)")
134
+ end
135
+ end
136
+
137
+ checks
138
+ end
139
+
140
+ def ok_check(label, message)
141
+ { state: "OK", label: label, message: message }
142
+ end
143
+
144
+ def warn_check(label, message)
145
+ { state: "WARN", label: label, message: message }
146
+ end
147
+
148
+ def fail_check(label, message)
149
+ { state: "FAIL", label: label, message: message }
150
+ end
151
+
152
+ def extract_shell_script_version(content)
153
+ escaped = Regexp.escape(ShellSetup::VERSION_MARKER_PREFIX)
154
+ match = content.match(/^\s*#\s*#{escaped}\s+(.+?)\s*$/)
155
+ return nil unless match
156
+
157
+ match[1]
158
+ end
159
+
160
+ def command_success?(*cmd)
161
+ _out, status = Open3.capture2e(env.to_h, *cmd)
162
+ status.success?
163
+ rescue StandardError
164
+ false
165
+ end
166
+
167
+ def fixture_mode?
168
+ value = env.fetch("DX_DOCTOR_FIXTURE_MODE", "")
169
+ %w[1 true yes on].include?(value.to_s.strip.downcase)
170
+ end
171
+ end
172
+ end
173
+ end
@@ -3,6 +3,17 @@
3
3
  module DevContext
4
4
  module Commands
5
5
  module GitOps
6
+ def cmd_import(emit_script: true)
7
+ imported_paths = parsed_shell_stack_paths
8
+ imported_contexts = import_contexts_for_paths(imported_paths)
9
+ config.active_stack.replace(imported_contexts.map { |ctx| ctx.fetch("name") })
10
+ config.send(:save!)
11
+ return 0 unless emit_script
12
+
13
+ out.write(stack_sync_script_for_active_contexts)
14
+ 0
15
+ end
16
+
6
17
  def cmd_active
7
18
  active = config.active_contexts
8
19
  if active.empty?
@@ -29,8 +40,35 @@ module DevContext
29
40
  0
30
41
  end
31
42
 
43
+ def cmd_push
44
+ target = argv.shift
45
+ return usage_error("dx pushd [CONTEXT|+N|-N]") unless argv.empty?
46
+
47
+ if target&.match?(/\A[+-]\d+\z/)
48
+ return rotate_active_stack_to(target)
49
+ end
50
+
51
+ return usage_error("dx pushd [CONTEXT|+N|-N]") if blank?(target)
52
+
53
+ context, code = resolve_or_create_context(target)
54
+ return code unless code.zero?
55
+
56
+ config.activate_context!(context.fetch("name"))
57
+ script = ShellEmitter.new(
58
+ context: context,
59
+ remote_name: env.fetch("DX_GIT_REMOTE_NAME", "USE-REPO"),
60
+ auto_create_local_branch: truthy?(env.fetch("DX_AUTO_CREATE_LOCAL_BRANCH", "true")),
61
+ stack_paths: config.active_contexts.map { |ctx| ctx.fetch("repo_path") }
62
+ ).activation_script
63
+ out.write(script)
64
+ 0
65
+ end
66
+
32
67
  def cmd_pop
33
68
  target = argv.shift
69
+ return usage_error("dx popd [CONTEXT|+N|-N]") unless argv.empty?
70
+ return pop_by_stack_index("+0") if target.nil?
71
+ return pop_by_stack_index(target) if target.match?(/\A[+-]\d+\z/)
34
72
 
35
73
  if target
36
74
  context = matcher.resolve(target)
@@ -39,7 +77,7 @@ module DevContext
39
77
  removed = config.deactivate_context!(context.fetch("name"))
40
78
  return not_found(target) unless removed
41
79
 
42
- out.puts("Popped #{context.fetch('name')}")
80
+ print_pop_message("Popped #{context.fetch('name')}")
43
81
  else
44
82
  top_name = config.active_stack.first
45
83
  if top_name.nil?
@@ -49,13 +87,10 @@ module DevContext
49
87
 
50
88
  context = config.contexts.fetch(top_name)
51
89
  config.deactivate_context!(top_name)
52
- out.puts("Popped #{context.fetch('name')}")
90
+ print_pop_message("Popped #{context.fetch('name')}")
53
91
  end
54
92
 
55
- new_top = config.active_contexts.first
56
- return 0 unless new_top
57
-
58
- out.puts("cd #{Shellwords.escape(new_top.fetch('repo_path'))}")
93
+ out.write(stack_sync_script_for_active_contexts)
59
94
  0
60
95
  end
61
96
 
@@ -158,6 +193,142 @@ module DevContext
158
193
  out.puts("Checked out #{feature} in #{context_name}")
159
194
  0
160
195
  end
196
+
197
+ private
198
+
199
+ def parsed_shell_stack_paths
200
+ seen = {}
201
+ env.fetch("DX_SHELL_STACK", "")
202
+ .lines
203
+ .map(&:strip)
204
+ .reject(&:empty?)
205
+ .map { |path| File.expand_path(path, pwd) }
206
+ .select { |path| git_repo?(path) }
207
+ .reject do |path|
208
+ already_seen = seen[path]
209
+ seen[path] = true
210
+ already_seen
211
+ end
212
+ end
213
+
214
+ def import_contexts_for_paths(paths)
215
+ paths.map do |repo_path|
216
+ branch = current_branch(repo_path) || "main"
217
+ context = config.contexts.values.find { |ctx| ctx["repo_path"] == repo_path && ctx["branch"] == branch } ||
218
+ config.contexts.values.find { |ctx| ctx["repo_path"] == repo_path }
219
+ unless context
220
+ name = implicit_context_name(repo_path, branch)
221
+ context_name = ensure_unique_import_context_name(name, repo_path, branch)
222
+ config.add_context(name: context_name, repo_path: repo_path, branch: branch)
223
+ context = config.get_context(context_name)
224
+ end
225
+ context
226
+ end.compact
227
+ end
228
+
229
+ def ensure_unique_import_context_name(base_name, repo_path, branch)
230
+ existing = config.contexts[base_name]
231
+ return base_name unless existing
232
+ return base_name if existing["repo_path"] == repo_path && existing["branch"] == branch
233
+
234
+ suffix = 2
235
+ loop do
236
+ candidate = "#{base_name}-#{suffix}"
237
+ match = config.contexts[candidate]
238
+ return candidate unless match
239
+ return candidate if match["repo_path"] == repo_path && match["branch"] == branch
240
+
241
+ suffix += 1
242
+ end
243
+ end
244
+
245
+ def shell_stack_imbalanced?
246
+ parsed_shell_stack_paths != config.active_contexts.map { |ctx| File.expand_path(ctx.fetch("repo_path")) }
247
+ end
248
+
249
+ def rotate_active_stack_to(token)
250
+ stack = config.active_stack
251
+ return no_active_contexts if stack.empty?
252
+
253
+ idx = parse_stack_index_token(token, stack.length)
254
+ if idx.nil?
255
+ err.puts("dx: invalid stack index '#{token}'")
256
+ return 2
257
+ end
258
+
259
+ context_name = stack[idx]
260
+ context = config.contexts[context_name]
261
+ unless context
262
+ err.puts("dx: active stack entry is missing context: #{context_name}")
263
+ return 1
264
+ end
265
+
266
+ config.active_stack.replace(stack.rotate(idx))
267
+ config.send(:save!)
268
+ script = ShellEmitter.new(
269
+ context: context,
270
+ remote_name: env.fetch("DX_GIT_REMOTE_NAME", "USE-REPO"),
271
+ auto_create_local_branch: truthy?(env.fetch("DX_AUTO_CREATE_LOCAL_BRANCH", "true")),
272
+ stack_paths: config.active_contexts.map { |ctx| ctx.fetch("repo_path") }
273
+ ).activation_script
274
+ out.write(script)
275
+ 0
276
+ end
277
+
278
+ def pop_by_stack_index(token)
279
+ stack = config.active_stack
280
+ return no_active_contexts if stack.empty?
281
+
282
+ idx = parse_stack_index_token(token, stack.length)
283
+ if idx.nil?
284
+ err.puts("dx: invalid stack index '#{token}'")
285
+ return 2
286
+ end
287
+
288
+ context_name = stack.delete_at(idx)
289
+ context = config.contexts[context_name]
290
+ config.send(:save!)
291
+ print_pop_message("Popped #{context ? context.fetch('name') : context_name}")
292
+ out.write(stack_sync_script_for_active_contexts)
293
+ 0
294
+ end
295
+
296
+ def parse_stack_index_token(token, stack_size)
297
+ return nil unless token.match?(/\A[+-]\d+\z/)
298
+
299
+ n = token[1..].to_i
300
+ idx = token.start_with?("+") ? n : (stack_size - 1 - n)
301
+ return nil if idx.negative? || idx >= stack_size
302
+
303
+ idx
304
+ end
305
+
306
+ def no_active_contexts
307
+ out.puts("No active contexts")
308
+ 0
309
+ end
310
+
311
+ def stack_sync_script_for_active_contexts
312
+ paths = config.active_contexts.map { |ctx| File.expand_path(ctx.fetch("repo_path")) }
313
+ script = +"#{ShellEmitter::SCRIPT_MARKER}\n"
314
+ script << "dirs -c >/dev/null 2>&1 || true\n"
315
+ return script if paths.empty?
316
+
317
+ script << "cd #{Shellwords.escape(paths.first)}\n"
318
+ paths.drop(1).each do |path|
319
+ script << "pushd #{Shellwords.escape(path)} >/dev/null 2>&1\n"
320
+ script << "pushd +1 >/dev/null 2>&1\n"
321
+ end
322
+ script
323
+ end
324
+
325
+ def print_pop_message(message)
326
+ if env["DX_SHELL_WRAPPED"] == "1"
327
+ err.puts(message)
328
+ else
329
+ out.puts(message)
330
+ end
331
+ end
161
332
  end
162
333
  end
163
334
  end
@@ -25,17 +25,21 @@ 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 import
37
+ dx version
38
+ dx popd [CONTEXT|+N|-N]
39
+ dx pushd [CONTEXT|PATH|URL|+N|-N]
37
40
  dx repos [PATTERN]
38
41
  dx remove CONTEXT|PATH
42
+ dx stash [--list] [--all|-a] [PATTERN]
39
43
  dx stashes [--list] [PATTERN]
40
44
  dx status|wip
41
45
  HELP
@@ -48,20 +52,32 @@ module DevContext
48
52
  "Usage: dx status [--all] [--dirty] [-b BRANCHPATTERN] [-p PATHPATTERN]\n dx wip [--all] [--dirty] [-b BRANCHPATTERN] [-p PATHPATTERN]\n\nShow branch and git status for active contexts. Use --all for all known repos, --dirty to filter non-clean repos, and -b/-p to filter by branch/path."
49
53
  when "repos"
50
54
  "Usage: dx repos [-b BRANCHPATTERN] [-p PATHPATTERN] [PATTERN]\n\nList all known repos without git status details. PATTERN is shorthand for path filtering."
55
+ when "stash"
56
+ "Usage: dx stash [--list] [--all|-a] [PATTERN]\n\nShow stashes for the active top context by default. Use --all/-a to scan all known repos. Provide PATTERN to filter stash titles; use --list to print matching entries."
51
57
  when "stashes"
52
58
  "Usage: dx stashes [--list] [PATTERN]\n\nShow repos with stashes. Provide PATTERN to filter stash titles; use --list to print matching entries."
53
59
  when "find"
54
60
  "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."
61
+ when "doctor", "check"
62
+ "Usage: dx doctor\n dx check\n\nRun environment and config diagnostics."
63
+ when "init"
64
+ "Usage: dx init [ruby|go|elixir]\n\nInitialize dx files. If IMPL is given, set DX_IMPL non-interactively."
65
+ when "version"
66
+ "Usage: dx version\n dx --version\n dx -V\n\nPrint the active dx implementation and version."
55
67
  when "add"
56
68
  "Usage: dx add [-c NAME] [-n] [-v] PATH|URL ..."
57
69
  when "clone"
58
70
  "Usage: dx clone [-c NAME] [-n] [-v] URL [PATH]"
59
71
  when "create"
60
72
  "Usage: dx create [-c NAME] [-n] [-v] NAME [PATH] [BRANCH]"
61
- when "activate", "cd", "pushd"
73
+ when "activate", "cd"
62
74
  "Usage: dx #{command} CONTEXT|PATH|URL"
75
+ when "pushd"
76
+ "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
77
  when "popd"
64
- "Usage: dx popd [CONTEXT]"
78
+ "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)."
79
+ when "import"
80
+ "Usage: dx import\n\nImport shell dirstack into dx active_stack (deduped by absolute path) and emit shell sync script."
65
81
  when "remove"
66
82
  "Usage: dx remove CONTEXT|PATH"
67
83
  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
@@ -3,6 +3,41 @@
3
3
  module DevContext
4
4
  module Commands
5
5
  module Stashes
6
+ def cmd_stash
7
+ args = argv.dup
8
+ show_list = false
9
+ all = false
10
+ parser = OptionParser.new
11
+ parser.on("-l", "--list", "Show stash entry titles per repo") { show_list = true }
12
+ parser.on("-a", "--all", "Show stashes across all known repos") { all = true }
13
+ parser.parse!(args)
14
+ pattern = args.shift
15
+ return usage_error("dx stash [--list] [--all|-a] [PATTERN]") unless args.empty?
16
+
17
+ rows = if all
18
+ known_repo_stash_rows(pattern: pattern)
19
+ else
20
+ active_top_stash_rows(pattern: pattern)
21
+ end
22
+ if rows.empty?
23
+ out.puts("No repos with stashes")
24
+ return 0
25
+ end
26
+
27
+ render_stash_table(rows)
28
+ return 0 unless show_list
29
+
30
+ out.puts
31
+ rows.each do |row|
32
+ out.puts("#{display_path(row[:path])}:")
33
+ row[:entries].each { |entry| out.puts(" - #{entry}") }
34
+ end
35
+ 0
36
+ rescue OptionParser::InvalidOption => e
37
+ err.puts("dx: #{e.message}")
38
+ usage_error("dx stash [--list] [--all|-a] [PATTERN]")
39
+ end
40
+
6
41
  def cmd_stashes
7
42
  args = argv.dup
8
43
  show_list = false
@@ -34,6 +69,26 @@ module DevContext
34
69
 
35
70
  private
36
71
 
72
+ def active_top_stash_rows(pattern: nil)
73
+ context = config.active_contexts.first
74
+ return [] unless context
75
+
76
+ path = context["repo_path"]
77
+ expanded = File.expand_path(path)
78
+ return [] unless File.directory?(expanded)
79
+
80
+ entries = stash_entries(expanded)
81
+ entries = entries.select { |entry| pattern_match?(entry, pattern) } if pattern
82
+ return [] if entries.empty?
83
+
84
+ [{
85
+ path: expanded,
86
+ branch: current_branch(expanded) || "-",
87
+ count: entries.length,
88
+ entries: entries
89
+ }]
90
+ end
91
+
37
92
  def render_stash_table(rows)
38
93
  display_rows = rows.map { |row| row.merge(path: display_path(row[:path])) }
39
94
  path_w = [display_rows.map { |r| r[:path].length }.max || 0, "Path".length].max
@@ -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
@@ -6,10 +6,11 @@ module DevContext
6
6
  class ShellEmitter
7
7
  SCRIPT_MARKER = "# DX_SHELL_EVAL".freeze
8
8
 
9
- def initialize(context:, remote_name:, auto_create_local_branch:)
9
+ def initialize(context:, remote_name:, auto_create_local_branch:, stack_paths: nil)
10
10
  @context = context
11
11
  @remote_name = remote_name
12
12
  @auto_create_local_branch = auto_create_local_branch
13
+ @stack_paths = Array(stack_paths).map { |path| File.expand_path(path) }
13
14
  end
14
15
 
15
16
  def activation_script
@@ -51,11 +52,25 @@ module DevContext
51
52
  git remote get-url #{forced_remote} >/dev/null 2>&1 || __dx_fail "dx: remote '#{forced_remote}' does not exist in this repo" 19
52
53
  git pull --ff-only --autostash #{forced_remote} #{branch} || __dx_fail "dx: pull failed from #{forced_remote}/#{branch} (you are already in the target repo for investigation)" 20
53
54
  fi
55
+ #{stack_sync_script}
54
56
  SH
55
57
  end
56
58
 
57
59
  private
58
60
 
59
- attr_reader :context, :remote_name, :auto_create_local_branch
61
+ attr_reader :context, :remote_name, :auto_create_local_branch, :stack_paths
62
+
63
+ def stack_sync_script
64
+ paths = stack_paths
65
+ return "dirs -c >/dev/null 2>&1 || true" if paths.empty?
66
+
67
+ lines = ["dirs -c >/dev/null 2>&1 || true", "cd #{Shellwords.escape(paths.first)} || __dx_fail \"dx: could not cd into #{Shellwords.escape(paths.first)}\" 21"]
68
+ paths.drop(1).each do |path|
69
+ escaped = Shellwords.escape(path)
70
+ lines << "pushd #{escaped} >/dev/null 2>&1 || __dx_fail \"dx: could not pushd #{escaped}\" 22"
71
+ lines << "pushd +1 >/dev/null 2>&1 || __dx_fail \"dx: could not rotate dirstack\" 23"
72
+ end
73
+ lines.join("\n")
74
+ end
60
75
  end
61
76
  end
@@ -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,32 @@ 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
42
55
  case "$1" in
43
- cd|activate|pushd|popd)
56
+ cd|activate|pushd|popd|pu|po|import)
44
57
  local out
45
- out="$(DX_SHELL_WRAPPED=1 command dev_context.rb "$@")" || return $?
58
+ out="$(DX_SHELL_WRAPPED=1 DX_SHELL_STACK="$(dirs -p 2>/dev/null || pwd)" command $bin "$@")" || return $?
46
59
  case "$out" in
47
60
  "# DX_SHELL_EVAL"*) eval "$out" ;;
48
61
  *) printf "%s\\n" "$out" ;;
49
62
  esac
50
63
  ;;
51
64
  *)
52
- command dev_context.rb "$@"
65
+ command $bin "$@"
53
66
  ;;
54
67
  esac
55
68
  }
@@ -1,8 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DevContext
4
+ SHELL_VERSION = "2"
4
5
  RELEASES = [
6
+ ["2.0.1", '2026-06-25'],
7
+ ["2.0.0", '2026-06-23'],
8
+ ["1.2.0", '2026-05-28'],
9
+ ["1.1.0", '2026-05-15'],
5
10
  ["1.0.1", '2026-05-12']
6
11
  ].freeze
7
- VERSION = RELEASES.last.first
12
+ VERSION = RELEASES[0][0]
8
13
  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,14 +1,29 @@
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: 2.0.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-06-26 00:00:00.000000000 Z
11
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: psych
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '5.4'
12
27
  - !ruby/object:Gem::Dependency
13
28
  name: fuubar
14
29
  requirement: !ruby/object:Gem::Requirement
@@ -53,12 +68,14 @@ dependencies:
53
68
  version: '3.0'
54
69
  description: |
55
70
  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.
71
+ switching, branch awareness, multi-repo awareness, and Git-aware introspection. It helps
72
+ you manage multiple active projects with an active stack.
58
73
  email:
59
74
  - aks@stebbens.org
60
75
  executables:
61
- - dev_context.rb
76
+ - dev_context
77
+ - dev_context-rb
78
+ - dev_context-ruby
62
79
  extensions: []
63
80
  extra_rdoc_files: []
64
81
  files:
@@ -68,11 +85,14 @@ files:
68
85
  - LICENSE.txt
69
86
  - README.md
70
87
  - bin/console
71
- - bin/dev_context.rb
88
+ - bin/dev_context
89
+ - bin/dev_context-rb
90
+ - bin/dev_context-ruby
72
91
  - bin/setup
73
92
  - lib/dev_context.rb
74
93
  - lib/dev_context/cli.rb
75
94
  - lib/dev_context/commands/context_lifecycle.rb
95
+ - lib/dev_context/commands/doctor.rb
76
96
  - lib/dev_context/commands/find.rb
77
97
  - lib/dev_context/commands/git_ops.rb
78
98
  - lib/dev_context/commands/help.rb
@@ -96,6 +116,7 @@ metadata:
96
116
  allowed_push_host: https://rubygems.org
97
117
  source_code_uri: https://github.com/aks/dev_context
98
118
  changelog_uri: https://github.com/aks/dev_context/blob/main/CHANGELOG.md
119
+ post_install_message:
99
120
  rdoc_options: []
100
121
  require_paths:
101
122
  - lib
@@ -110,7 +131,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
110
131
  - !ruby/object:Gem::Version
111
132
  version: '0'
112
133
  requirements: []
113
- rubygems_version: 4.0.11
134
+ rubygems_version: 3.5.22
135
+ signing_key:
114
136
  specification_version: 4
115
137
  summary: A developer context manager for multi-repo workflows
116
138
  test_files: []
File without changes