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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +15 -9
- data/bin/dev_context +35 -0
- data/lib/dev_context/cli.rb +33 -22
- data/lib/dev_context/commands/doctor.rb +167 -0
- data/lib/dev_context/commands/git_ops.rb +92 -0
- data/lib/dev_context/commands/help.rb +15 -5
- data/lib/dev_context/commands/init.rb +60 -0
- data/lib/dev_context/commands/support.rb +3 -2
- data/lib/dev_context/shell_setup.rb +16 -2
- data/lib/dev_context/version.rb +4 -1
- data/lib/dev_context.rb +1 -0
- metadata +13 -7
- /data/bin/{dev_context.rb → dev_context-ruby} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5acaef7b6cb13adb292bcdbc2f59bff0a659517d3b4d17f36f23b8afb11ee40d
|
|
4
|
+
data.tar.gz: c5d9cb92a6785bc719b810d66ea18badfd201c2c3fdf9ae438a5e4c41fca6994
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
85
|
-
- `dx popd [CONTEXT]`: pop
|
|
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
|
data/lib/dev_context/cli.rb
CHANGED
|
@@ -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 "
|
|
66
|
-
when "
|
|
67
|
-
when "
|
|
68
|
-
when "
|
|
69
|
-
when "
|
|
70
|
-
when "
|
|
71
|
-
when "
|
|
72
|
-
when "
|
|
73
|
-
when "
|
|
74
|
-
when "
|
|
75
|
-
when "
|
|
76
|
-
when "
|
|
77
|
-
when "
|
|
78
|
-
when "
|
|
79
|
-
when "
|
|
80
|
-
when "
|
|
81
|
-
when "
|
|
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
|
|
36
|
-
dx
|
|
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"
|
|
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
|
-
|
|
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
|
|
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
|
|
66
|
+
command $bin "$@"
|
|
53
67
|
;;
|
|
54
68
|
esac
|
|
55
69
|
}
|
data/lib/dev_context/version.rb
CHANGED
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.
|
|
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:
|
|
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
|
|
57
|
-
active projects with an
|
|
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
|
|
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
|
|
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:
|
|
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
|