dip 8.3.0 → 8.4.0

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: b1251c437797a56125f593a350dde6c3709f70936a6cc274949d0b2b58dd814f
4
- data.tar.gz: d1e4d3484bdc41ed6281d14a7ce324146cd1768989c3e3f3fa09fc412d72bf2d
3
+ metadata.gz: eae945780bd009e844b733abddfbc4529204ff443a4b112b39e13dfdc4f8e987
4
+ data.tar.gz: 60790bd09dda4ebda2c18a08c774b5deb9e52336c51723fa153dc034376c8806
5
5
  SHA512:
6
- metadata.gz: 1dafa2c574e88f414a498ed0e3cbeaab047085ee0232977fcf1900ef92637c333cab39dc9d9ae1e5eaf93843949e3bd62a9c61f23f46ba83711cd0aecf83412c
7
- data.tar.gz: 3ce49864159703c73ac8811a5a9f8d295192f3dc0be569a7d6708a5084013a2464de89b67cb1982edd888dec7284d44761959b85fa02b62e23b95547b5ef70fc
6
+ metadata.gz: 4e881c360bfb42ef5e6e3ca150e656b0cca36adbbf171424ca4a7fc78771bd15b6572a7438aae53009a73123b541ab47af1afd873f1734263a3bfde6d400f959
7
+ data.tar.gz: 93fadd1ead85482d4b9b7a128cfbd5e75de0df7e2d575f9d9ba766c86a7221c4b3ec32bbb999e6041bf2418b58cade580cb3c9e9e0160d2f4dc797e139c15974
data/README.md CHANGED
@@ -36,12 +36,34 @@ gem install dip
36
36
 
37
37
  ### Integration with shell
38
38
 
39
- Dip can be injected into the current shell (ZSH or Bash).
39
+ Dip can be injected into the current shell so that interaction commands (and `compose`, `up`, `stop`, `down`, `build`, `provision`) become available without the `dip` prefix. **Bash**, **ZSH**, and **Fish** are supported.
40
+
41
+ Add the matching line to your shell startup file so the integration is loaded in every session:
42
+
43
+ ```sh
44
+ # Bash — ~/.bashrc or ~/.bash_profile
45
+ eval "$(dip console)"
46
+ ```
40
47
 
41
48
  ```sh
49
+ # ZSH — ~/.zshrc
42
50
  eval "$(dip console)"
43
51
  ```
44
52
 
53
+ ```fish
54
+ # Fish — ~/.config/fish/config.fish
55
+ dip console | source
56
+ ```
57
+
58
+ The target shell is autodetected from the `$SHELL` environment variable, so the snippets above work as-is. If autodetection is wrong (for example, you run Fish but `$SHELL` still points to Bash), force the dialect explicitly:
59
+
60
+ ```sh
61
+ eval "$(dip console --shell zsh)" # Bash / ZSH
62
+ dip console --shell fish | source # Fish
63
+ ```
64
+
65
+ `--shell` accepts `bash`, `zsh`, or `fish` (`bash` and `zsh` produce the same POSIX output).
66
+
45
67
  **IMPORTANT**: Beware of possible collisions with local tools. One particular example is supporting both local and Docker frontend build tools, such as Yarn. If you want some developer to run `yarn` locally and other to use Docker for that, you should either avoid adding the `yarn` command to the `dip.yml` or avoid using the shell integration for hybrid development.
46
68
 
47
69
  After that we can type commands without `dip` prefix. For example:
@@ -54,16 +76,27 @@ ktl *any-kubectl-arg
54
76
  provision
55
77
  ```
56
78
 
57
- When we change the current directory, all shell aliases will be automatically removed. But when we enter back into a directory with a `dip.yml` file, then shell aliases will be renewed.
79
+ When we change the current directory, all shell aliases are automatically removed. When we enter a directory that has a `dip.yml` file (in it or in a parent), the aliases are renewed. This is wired through the shell's directory-change hook `chpwd_functions` on ZSH, a `cd`/`pushd`/`popd` wrapper on Bash, and a `--on-variable PWD` handler on Fish. The hook resolves the applicable `dip.yml` itself (no `dip` process involved) and only actually reloads when that path changes, so `cd`ing around inside the same project doesn't re-run `dip` on every prompt.
58
80
 
59
- Also, in shell mode Dip is trying to determine manually passed environment variables. For example:
81
+ Also, in shell mode Dip tries to determine manually passed environment variables. For example:
60
82
 
61
83
  ```sh
62
84
  VERSION=20180515103400 rails db:migrate:down
63
85
  ```
64
86
 
65
- You could add this `eval` at the end of your `~/.zshrc`, or `~/.bashrc`, or `~/.bash_profile`.
66
- After that, it will be automatically applied when you open your preferred terminal.
87
+ Once the startup snippet is in place, the integration is applied automatically every time you open your terminal.
88
+
89
+ #### How it works
90
+
91
+ `dip console` prints a bootstrap script that defines three helpers:
92
+
93
+ - `dip_inject` — evaluates `dip console inject`, which emits one shell function per interaction command plus the built-in `compose`/`up`/`stop`/`down`/`build`/`provision` wrappers. An interaction command whose name collides with a shell builtin/keyword (`jobs`, `test`, `read`, `set`, …) is skipped, with a warning on stderr — run it as `dip <name>` instead, or rename it.
94
+ - `dip_clear` — removes the functions previously injected (regenerated on every inject so it always matches the current `dip.yml`).
95
+ - `dip_reload` — runs `dip_clear` then `dip_inject`; also bound to the directory-change hook, which calls it only when the resolved `dip.yml` path actually changed.
96
+
97
+ The bootstrap also exports `DIP_SHELL=1`, `DIP_EARLY_ENVS` (the list of variables present at load time, used to detect manually passed env vars), and `DIP_PROMPT_TEXT` (`ⅆ`). On ZSH with the `agnoster` theme, `DIP_PROMPT_TEXT` is added as a prompt segment; other shells and themes are left untouched.
98
+
99
+ You can force a reload at any time by running `dip_reload` (useful after editing `dip.yml`).
67
100
 
68
101
  ## Usage
69
102
 
@@ -508,6 +541,20 @@ If validation fails, you'll get detailed error messages indicating what needs to
508
541
  You can skip validation by setting `DIP_SKIP_VALIDATION` environment variable.
509
542
 
510
543
  Add `# yaml-language-server: $schema=https://raw.githubusercontent.com/bibendi/dip/refs/heads/master/schema.json` to the top of your dip.yml to get schema validation in VSCode. Read more about [YAML Language Server](https://github.com/redhat-developer/vscode-yaml?tab=readme-ov-file#associating-schemas).
544
+
545
+ ### dip console
546
+
547
+ Prints the shell integration script for the current shell. See [Integration with shell](#integration-with-shell) for the full setup.
548
+
549
+ ```sh
550
+ dip console [--shell bash|zsh|fish] # bootstrap script (default subcommand)
551
+ dip console inject [--shell bash|zsh|fish] # just the command aliases
552
+ ```
553
+
554
+ - `--shell` (`-s`) selects the dialect: `bash`, `zsh`, or `fish`. When omitted, it is autodetected from `$SHELL`, falling back to POSIX (Bash/ZSH) output.
555
+ - `dip console` is meant to be evaluated by your shell: `eval "$(dip console)"` for Bash/ZSH, `dip console | source` for Fish.
556
+ - `dip console inject` is called internally by the bootstrap script (via `dip_reload`); you normally don't run it by hand. It only needs the interaction command names, so it skips full `dip.yml` schema validation — run `dip validate` (or any other `dip` command) to check the file against the schema.
557
+
511
558
  ## Changelog
512
559
 
513
560
  See [CHANGELOG.md](CHANGELOG.md).
@@ -7,14 +7,21 @@ require_relative "../commands/console"
7
7
  module Dip
8
8
  class CLI
9
9
  class Console < Base
10
+ SHELL_OPTION = [
11
+ :shell,
12
+ {aliases: "-s", type: :string, enum: %w[bash zsh fish posix],
13
+ desc: "Target shell dialect (autodetected from $SHELL by default)"}
14
+ ].freeze
15
+
10
16
  desc "start", "Integrate Dip into current shell"
11
17
  method_option :help, aliases: "-h", type: :boolean,
12
18
  desc: "Display usage information"
19
+ method_option(*SHELL_OPTION)
13
20
  def start
14
21
  if options[:help]
15
22
  invoke :help, ["start"]
16
23
  else
17
- Dip::Commands::Console::Start.new.execute
24
+ Dip::Commands::Console::Start.new(shell: options[:shell]).execute
18
25
  end
19
26
  end
20
27
 
@@ -23,11 +30,12 @@ module Dip
23
30
  desc "inject", "Inject aliases"
24
31
  method_option :help, aliases: "-h", type: :boolean,
25
32
  desc: "Display usage information"
33
+ method_option(*SHELL_OPTION)
26
34
  def inject
27
35
  if options[:help]
28
36
  invoke :help, ["inject"]
29
37
  else
30
- Dip::Commands::Console::Inject.new.execute
38
+ Dip::Commands::Console::Inject.new(shell: options[:shell]).execute
31
39
  end
32
40
  end
33
41
  end
data/lib/dip/cli.rb CHANGED
@@ -143,7 +143,7 @@ module Dip
143
143
  subcommand :infra, Dip::CLI::Infra
144
144
 
145
145
  require_relative "cli/console"
146
- desc "console", "Integrate Dip commands into shell (only ZSH and Bash are supported)"
146
+ desc "console", "Integrate Dip commands into shell (Bash, ZSH and Fish are supported)"
147
147
  subcommand :console, Dip::CLI::Console
148
148
  end
149
149
  end
@@ -5,14 +5,50 @@ require_relative "../command"
5
5
  module Dip
6
6
  module Commands
7
7
  module Console
8
+ # Interaction command names come straight from `dip.yml`, and `inject`
9
+ # turns each one into a same-named shell function. A name that collides
10
+ # with a shell builtin/keyword silently shadows it for the rest of the
11
+ # session — e.g. an interaction called `jobs` breaks `jobs`, which
12
+ # things like Starship's prompt call on every render, turning every
13
+ # keystroke into a `dip jobs` invocation. Skip those instead of
14
+ # aliasing over them.
15
+ RESERVED_NAMES = %w[
16
+ cd pwd test read set jobs type command builtin history alias unalias
17
+ source eval exec exit return break continue trap kill wait bg fg
18
+ export unset declare typeset readonly local shift times true false
19
+ echo printf time status functions function end
20
+ ].freeze
21
+
22
+ # Figure out which shell dialect to generate integration code for.
23
+ #
24
+ # An explicit value (from `--shell`) always wins. Otherwise we guess from
25
+ # the `$SHELL` environment variable and fall back to the POSIX flavour
26
+ # (bash/zsh) which was the only supported one historically.
27
+ def self.detect_shell(explicit = nil)
28
+ name = (explicit || File.basename(ENV["SHELL"].to_s)).to_s.downcase
29
+ name.include?("fish") ? :fish : :posix
30
+ end
31
+
8
32
  class Start < Dip::Command
33
+ def initialize(shell: nil)
34
+ @shell = Console.detect_shell(shell)
35
+ end
36
+
9
37
  def execute
10
- puts script
38
+ puts fish? ? fish_script : posix_script
11
39
  end
12
40
 
13
41
  private
14
42
 
15
- def script
43
+ def fish?
44
+ @shell == :fish
45
+ end
46
+
47
+ def inject_command
48
+ "#{Dip.bin_path} console inject --shell #{@shell}"
49
+ end
50
+
51
+ def posix_script
16
52
  <<-SH.gsub(/^ {12}/, "")
17
53
  export DIP_SHELL=1
18
54
  export DIP_EARLY_ENVS=#{ENV.keys.join(",")}
@@ -24,7 +60,7 @@ module Dip
24
60
  }
25
61
 
26
62
  function dip_inject() {
27
- eval "$(#{Dip.bin_path} console inject)"
63
+ eval "$(#{inject_command})"
28
64
  }
29
65
 
30
66
  function dip_reload() {
@@ -32,6 +68,36 @@ module Dip
32
68
  dip_inject
33
69
  }
34
70
 
71
+ # Resolves the dip.yml that applies to $PWD (or $DIP_FILE), without
72
+ # spawning a `dip` process — used to skip redundant reloads below.
73
+ function __dip_config_path() {
74
+ if [ -n "${DIP_FILE:-}" ]; then
75
+ printf '%s\\n' "$DIP_FILE"
76
+ return
77
+ fi
78
+
79
+ \\typeset __dip_dir="$PWD"
80
+ while :; do
81
+ if [ -e "$__dip_dir/dip.yml" ]; then
82
+ printf '%s\\n' "$__dip_dir/dip.yml"
83
+ return
84
+ fi
85
+ [ "$__dip_dir" = "/" ] && return
86
+ __dip_dir=$(dirname "$__dip_dir")
87
+ done
88
+ }
89
+
90
+ # Only reload aliases on `cd` when the resolved dip.yml actually
91
+ # changed — most `cd`s stay within the same project and would
92
+ # otherwise re-spawn `dip` (Ruby boot + schema validation) for nothing.
93
+ function __dip_auto_reload() {
94
+ \\typeset __dip_new_config_path
95
+ __dip_new_config_path="$(__dip_config_path)"
96
+ [ "$__dip_new_config_path" = "${__DIP_CONFIG_PATH:-}" ] && return
97
+ __DIP_CONFIG_PATH="$__dip_new_config_path"
98
+ dip_reload
99
+ }
100
+
35
101
  # Inspired by RVM
36
102
  function __zsh_like_cd() {
37
103
  \\typeset __zsh_like_cd_hook
@@ -58,7 +124,7 @@ module Dip
58
124
  }
59
125
 
60
126
  export -a chpwd_functions
61
- [[ " ${chpwd_functions[*]} " == *" dip_reload "* ]] || chpwd_functions+=(dip_reload)
127
+ [[ " ${chpwd_functions[*]} " == *" __dip_auto_reload "* ]] || chpwd_functions+=(__dip_auto_reload)
62
128
 
63
129
  if [[ "$ZSH_THEME" = "agnoster" ]]; then
64
130
  eval "`declare -f prompt_end | sed '1s/.*/_&/'`"
@@ -73,22 +139,87 @@ module Dip
73
139
  fi
74
140
 
75
141
  dip_reload
142
+ __DIP_CONFIG_PATH="$(__dip_config_path)"
76
143
  SH
77
144
  end
145
+
146
+ def fish_script
147
+ <<-FISH.gsub(/^ {12}/, "")
148
+ set -gx DIP_SHELL 1
149
+ set -gx DIP_EARLY_ENVS "#{ENV.keys.join(",")}"
150
+ set -gx DIP_PROMPT_TEXT "ⅆ"
151
+
152
+ function dip_clear
153
+ # just stub, will be redefined after injecting aliases
154
+ true
155
+ end
156
+
157
+ function dip_inject
158
+ #{inject_command} | source
159
+ end
160
+
161
+ function dip_reload
162
+ dip_clear
163
+ dip_inject
164
+ end
165
+
166
+ # Resolves the dip.yml that applies to $PWD (or $DIP_FILE), without
167
+ # spawning a `dip` process — used to skip redundant reloads below.
168
+ function __dip_config_path
169
+ if set -q DIP_FILE
170
+ echo "$DIP_FILE"
171
+ return
172
+ end
173
+
174
+ set -l __dip_dir "$PWD"
175
+ while true
176
+ if test -e "$__dip_dir/dip.yml"
177
+ echo "$__dip_dir/dip.yml"
178
+ return
179
+ end
180
+ if test "$__dip_dir" = "/"
181
+ return
182
+ end
183
+ set __dip_dir (dirname "$__dip_dir")
184
+ end
185
+ end
186
+
187
+ # Renew aliases whenever the working directory changes — but only
188
+ # when the resolved dip.yml actually changed. Most `cd`s stay within
189
+ # the same project and would otherwise re-spawn `dip` (Ruby boot +
190
+ # schema validation) for nothing.
191
+ function __dip_chpwd --on-variable PWD
192
+ set -l __dip_new_config_path (__dip_config_path)
193
+ if test "$__dip_new_config_path" != "$__dip_config_path_cache"
194
+ set -g __dip_config_path_cache "$__dip_new_config_path"
195
+ dip_reload
196
+ end
197
+ end
198
+
199
+ dip_reload
200
+ set -g __dip_config_path_cache (__dip_config_path)
201
+ FISH
202
+ end
78
203
  end
79
204
 
80
205
  class Inject < Dip::Command
81
206
  attr_reader :out, :aliases
82
207
 
83
- def initialize
208
+ def initialize(shell: nil)
209
+ @shell = Console.detect_shell(shell)
84
210
  @aliases = []
85
211
  @out = []
86
212
  end
87
213
 
88
214
  def execute
89
- if Dip.config.exist?
90
- add_aliases(*Dip.config.interaction.keys) if Dip.config.interaction
91
- add_aliases("compose", "up", "stop", "down", "provision", "build")
215
+ # Runs on every automatic shell reload (e.g. on `cd`), so it only needs
216
+ # the interaction command names, not full schema conformance — skip the
217
+ # `json-schema` require and validation pass that every other command pays.
218
+ with_validation_skipped do
219
+ if Dip.config.exist?
220
+ add_aliases(*Dip.config.interaction.keys) if Dip.config.interaction
221
+ add_aliases("compose", "up", "stop", "down", "provision", "build")
222
+ end
92
223
  end
93
224
 
94
225
  clear_aliases
@@ -98,17 +229,49 @@ module Dip
98
229
 
99
230
  private
100
231
 
232
+ def with_validation_skipped
233
+ had_key = ENV.key?("DIP_SKIP_VALIDATION")
234
+ previous = ENV["DIP_SKIP_VALIDATION"]
235
+ ENV["DIP_SKIP_VALIDATION"] = "1"
236
+ yield
237
+ ensure
238
+ if had_key
239
+ ENV["DIP_SKIP_VALIDATION"] = previous
240
+ else
241
+ ENV.delete("DIP_SKIP_VALIDATION")
242
+ end
243
+ end
244
+
245
+ def fish?
246
+ @shell == :fish
247
+ end
248
+
101
249
  def add_aliases(*names)
102
250
  names.each do |name|
251
+ if Console::RESERVED_NAMES.include?(name.to_s)
252
+ warn "dip: skipping alias `#{name}` — it would shadow the `#{name}` shell builtin. " \
253
+ "Run it as `#{Dip.bin_path} #{name}`, or rename it in dip.yml."
254
+ next
255
+ end
256
+
103
257
  aliases << name
104
- out << "function #{name}() { #{Dip.bin_path} #{name} $@; }"
258
+ out << if fish?
259
+ "function #{name}; #{Dip.bin_path} #{name} $argv; end"
260
+ else
261
+ "function #{name}() { #{Dip.bin_path} #{name} $@; }"
262
+ end
105
263
  end
106
264
  end
107
265
 
108
266
  def clear_aliases
109
- out << "function dip_clear() { \n" \
110
- "#{aliases.any? ? aliases.map { |a| " unset -f #{a}" }.join("\n") : "true"} " \
111
- "\n}"
267
+ out << if fish?
268
+ body = aliases.any? ? "functions -e #{aliases.join(" ")}" : "true"
269
+ "function dip_clear; #{body}; end"
270
+ else
271
+ "function dip_clear() { \n" \
272
+ "#{aliases.any? ? aliases.map { |a| " unset -f #{a}" }.join("\n") : "true"} " \
273
+ "\n}"
274
+ end
112
275
  end
113
276
  end
114
277
  end
data/lib/dip/config.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require "yaml"
4
4
  require "erb"
5
5
  require "pathname"
6
- require "json-schema"
6
+ require "json"
7
7
 
8
8
  require "dip/version"
9
9
  require "dip/ext/hash"
@@ -114,15 +114,22 @@ module Dip
114
114
  end
115
115
  end
116
116
 
117
- def validate
117
+ # `data`, when given, is the already-parsed raw config (as produced by
118
+ # `load_yaml`) so callers that parsed the file themselves don't pay for
119
+ # parsing it a second time here.
120
+ def validate(data = nil)
118
121
  raise Dip::Error, "Config file path is not set" if file_path.nil?
119
122
  raise Dip::Error, "Config file not found: #{file_path}" unless File.exist?(file_path)
120
123
 
121
124
  schema_path = File.join(File.dirname(__FILE__), "../../schema.json")
122
125
  raise Dip::Error, "Schema file not found: #{schema_path}" unless File.exist?(schema_path)
123
126
 
124
- data = self.class.load_yaml(file_path)
125
- schema = JSON::Validator.parse(File.read(schema_path))
127
+ require "json-schema"
128
+
129
+ data ||= self.class.load_yaml(file_path)
130
+ # Parse with the stdlib rather than JSON::Validator.parse: the latter passes
131
+ # `quirks_mode:` to JSON.parse, which the json gem removed in 3.0 (Ruby 3.5+).
132
+ schema = JSON.parse(File.read(schema_path))
126
133
  JSON::Validator.validate!(schema, data)
127
134
  rescue Psych::SyntaxError => e
128
135
  raise Dip::Error, "Invalid YAML syntax in config file: #{e.message}"
@@ -130,7 +137,7 @@ module Dip
130
137
  data_display = data ? data.to_yaml.gsub("\n", "\n ") : "nil"
131
138
  error_message = "Schema validation failed: #{e.message}\nInput data:\n #{data_display}"
132
139
  raise Dip::Error, error_message
133
- rescue JSON::Schema::JsonParseError => e
140
+ rescue JSON::Schema::JsonParseError, JSON::ParserError => e
134
141
  raise Dip::Error, "Error parsing schema file: #{e.message}"
135
142
  end
136
143
 
@@ -179,7 +186,7 @@ module Dip
179
186
  @config = CONFIG_DEFAULTS.merge(base_config)
180
187
 
181
188
  unless ENV.key?("DIP_SKIP_VALIDATION")
182
- validate
189
+ validate(config)
183
190
  end
184
191
 
185
192
  @config
data/lib/dip/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dip
4
- VERSION = "8.3.0"
4
+ VERSION = "8.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dip
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.3.0
4
+ version: 8.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - bibendi