Module: Dkit::Commands

Defined in:
lib/dkit/commands.rb

Class Method Summary collapse

Class Method Details

.cmd_claude(ctx, args) ⇒ Object



108
109
110
111
# File 'lib/dkit/commands.rb', line 108

def cmd_claude(ctx, args)
  warn "\e[32m[dkit] claude → #{ctx.container}\e[0m" if Intercept.verbose_enabled?(ctx.project_root)
  exec("docker", "exec", "-it", "--user", ctx.user, "--workdir", ctx.cwd, ctx.container, "claude", *args)
end

.cmd_code(ctx, path_arg) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/dkit/commands.rb', line 113

def cmd_code(ctx, path_arg)
  warn "\e[32m[dkit] code → #{ctx.container}\e[0m" if Intercept.verbose_enabled?(ctx.project_root)
  host_path = path_arg ? File.expand_path(path_arg) : ctx.project_root
  rel = begin
    Pathname(host_path).relative_path_from(Pathname(ctx.project_root)).to_s
  rescue ArgumentError
    "."
  end
  container_path = (rel == ".") ? ctx.workspace : File.join(ctx.workspace, rel)

  payload = JSON.generate({ "hostPath" => ctx.project_root })
  hex     = payload.unpack1("H*")
  uri     = "vscode-remote://dev-container+#{hex}#{container_path}"

  if system("which code > /dev/null 2>&1")
    exec("code", "--folder-uri", uri)
  elsif system("which devcontainer > /dev/null 2>&1")
    exec("devcontainer", "open", ctx.project_root)
  else
    Dkit.abort_err("'code' CLI not found. In VS Code: Shell Command: Install 'code' command in PATH")
  end
end

.cmd_compose(ctx, args) ⇒ Object



158
159
160
161
# File 'lib/dkit/commands.rb', line 158

def cmd_compose(ctx, args)
  files_flags = ctx.compose_files.flat_map { |f| ["-f", f] }
  exec("docker", "compose", *files_flags, *args)
end

.cmd_down(argv) ⇒ Object



172
173
174
175
# File 'lib/dkit/commands.rb', line 172

def cmd_down(argv)
  ctx = Dkit.resolve!
  cmd_compose(ctx, ["down", *argv])
end

.cmd_exec(ctx, args) ⇒ Object



91
92
93
94
95
96
# File 'lib/dkit/commands.rb', line 91

def cmd_exec(ctx, args)
  Dkit.abort_err("exec: no command given") if args.empty?
  warn "\e[32m[dkit] #{args.join(" ")}#{ctx.container}\e[0m" if Intercept.verbose_enabled?(ctx.project_root)
  system("docker", "exec", "--user", ctx.user, "--workdir", ctx.cwd, ctx.container, *args)
  exit $?.exitstatus
end

.cmd_helpObject



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/dkit/commands.rb', line 182

def cmd_help
  puts <<~HELP
    dkit #{VERSION} — DevKit: routes commands into your devcontainer

    Usage:
      dkit exec <cmd> [args]          Run command without TTY (scripting)
      dkit run  <cmd> [args]          Run command interactively (TTY)
      dkit shell                      Open interactive shell (zsh) in container
      dkit code  [path]               Open VS Code attached to devcontainer
      dkit claude [args]              Run claude in container (interactive)

      dkit status                     Show resolved devcontainer context
      dkit status --quiet             Exit 0 if container running, 1 otherwise
      dkit root                       Print project root (no docker needed)

      dkit up    [service]            docker compose up -d
      dkit down  [flags]              docker compose down
      dkit logs  [service]            docker compose logs -f

      dkit init                       Create .devcontainer/dkit-intercept with auto-detected defaults
      dkit intercept list             List intercepted commands for current project
      dkit intercept add <cmd>        Add command to current project's intercept list
      dkit intercept add 'bin/*'      Add glob pattern (quote to prevent shell expansion)
      dkit intercept remove <cmd>     Remove command from current project's intercept list

    Verbose routing messages (on by default):
      Add 'verbose: false' to .devcontainer/dkit-intercept  (per project, committed)
      Export DKIT_VERBOSE=0                                  (personal override)

      dkit hook                       Emit shell hook code for ~/.zshrc
      dkit version                    Print version
      dkit help                       Show this help

    Shell integration (add to ~/.zshrc):
      eval "$(dkit hook)"

    Project setup:
      cd ~/projects/my-app
      dkit init                       # creates .devcontainer/dkit-intercept
      git add .devcontainer/dkit-intercept && git commit -m "chore: add dkit config"

    Adding a new command to a project:
      dkit intercept add terraform
      exec zsh
  HELP
end

.cmd_hookObject



60
61
62
# File 'lib/dkit/commands.rb', line 60

def cmd_hook
  puts ShellHook.generate
end

.cmd_initObject



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/dkit/commands.rb', line 37

def cmd_init
  root = Project.find_root
  Dkit.abort_err("no #{DC_CONFIG} found — are you inside a devcontainer project?") unless root

  f = Intercept.file_path(root)
  if File.exist?(f)
    puts "dkit: #{f} already exists:"
    puts File.read(f)
    return
  end

  cmds = []
  cmds += %w[rails bundle rspec rubocop rake] if File.exist?(File.join(root, "Gemfile"))
  cmds += %w[yarn node npx]                   if File.exist?(File.join(root, "package.json"))
  cmds = %w[bash]                             if cmds.empty?

  File.write(f, "# verbose: false  # uncomment to suppress routing messages\n" + cmds.join("\n") + "\n")
  puts "dkit: created #{f}"
  puts "Commands: #{cmds.join(", ")}"
  puts "Tip: commit this file to share with your team"
  puts "     git add #{DC_INTERCEPT} && git commit -m 'chore: add dkit intercept config'"
end

.cmd_intercept(argv) ⇒ Object



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/dkit/commands.rb', line 64

def cmd_intercept(argv)
  root = Project.find_root
  Dkit.abort_err("no #{DC_CONFIG} found — are you inside a devcontainer project?") unless root

  sub = argv.shift&.downcase
  case sub
  when "list"
    list = Intercept.list(root)
    f    = Intercept.file_path(root)
    if list.empty?
      puts "No intercept file found. Run: dkit init"
    else
      puts "Intercepted commands (#{f}):"
      list.each { |c| puts "  #{c}" }
    end
    puts "\nSpecial (always active): #{SPECIAL_COMMANDS.join(", ")}"
  when "add"
    Dkit.abort_err("intercept add: command name required") if argv.empty?
    Intercept.add(root, argv.first)
  when "remove"
    Dkit.abort_err("intercept remove: command name required") if argv.empty?
    Intercept.remove(root, argv.first)
  else
    Dkit.abort_err("intercept: unknown subcommand '#{sub}'. Use: list, add, remove")
  end
end

.cmd_logs(argv) ⇒ Object



177
178
179
180
# File 'lib/dkit/commands.rb', line 177

def cmd_logs(argv)
  ctx = Dkit.resolve!
  cmd_compose(ctx, ["logs", "-f", *argv])
end

.cmd_rootObject



32
33
34
35
# File 'lib/dkit/commands.rb', line 32

def cmd_root
  root = Project.find_root
  root ? puts(root) : exit(1)
end

.cmd_run(ctx, args) ⇒ Object



98
99
100
101
102
# File 'lib/dkit/commands.rb', line 98

def cmd_run(ctx, args)
  Dkit.abort_err("run: no command given") if args.empty?
  warn "\e[32m[dkit] #{args.join(" ")}#{ctx.container}\e[0m" if Intercept.verbose_enabled?(ctx.project_root)
  exec("docker", "exec", "-it", "--user", ctx.user, "--workdir", ctx.cwd, ctx.container, *args)
end

.cmd_shell(ctx) ⇒ Object



104
105
106
# File 'lib/dkit/commands.rb', line 104

def cmd_shell(ctx)
  system("docker", "exec", "-it", "--user", ctx.user, "--workdir", ctx.cwd, ctx.container, "zsh", "-l")
end

.cmd_status(ctx, quiet:) ⇒ Object



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/dkit/commands.rb', line 142

def cmd_status(ctx, quiet:)
  return if quiet
  puts "Project root  : #{ctx.project_root}"
  puts "Container     : #{ctx.container} (running)"
  puts "Remote user   : #{ctx.user}"
  puts "Workspace     : #{ctx.workspace}"
  puts "Exec CWD      : #{ctx.cwd}"
  puts "Compose files : #{ctx.compose_files.join(", ")}"
  f = Intercept.file_path(ctx.project_root)
  if File.exist?(f)
    puts "Intercept     : #{Intercept.list(ctx.project_root).join(", ")}"
  else
    puts "Intercept     : (none — run 'dkit init')"
  end
end

.cmd_status_dispatch(argv) ⇒ Object



136
137
138
139
140
# File 'lib/dkit/commands.rb', line 136

def cmd_status_dispatch(argv)
  quiet = argv.include?("--quiet")
  ctx   = Dkit.resolve!(quiet: quiet)
  cmd_status(ctx, quiet: quiet)
end

.cmd_up(argv) ⇒ Object



163
164
165
166
167
168
169
170
# File 'lib/dkit/commands.rb', line 163

def cmd_up(argv)
  ctx = Dkit.resolve!(quiet: true) rescue nil
  if ctx
    cmd_compose(ctx, ["up", "-d", *argv])
  else
    exec("docker", "compose", "up", "-d", *argv)
  end
end

.dispatch(argv) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/dkit/commands.rb', line 5

def dispatch(argv)
  command = argv.shift&.downcase

  case command
  when "hook"         then cmd_hook
  when "root"         then cmd_root
  when "init"         then cmd_init
  when "intercept"    then cmd_intercept(argv)
  when "status"       then cmd_status_dispatch(argv)
  when "exec"         then cmd_exec(Dkit.resolve!, argv)
  when "run"          then cmd_run(Dkit.resolve!, argv)
  when "shell"        then cmd_shell(Dkit.resolve!)
  when "claude"       then cmd_claude(Dkit.resolve!, argv)
  when "code"         then cmd_code(Dkit.resolve!, argv.first)
  when "up"           then cmd_up(argv)
  when "down"         then cmd_down(argv)
  when "logs"         then cmd_logs(argv)
  when "version", "--version", "-v"
    puts "dkit #{VERSION}"
  when nil, "help", "--help", "-h"
    cmd_help
  else
    warn "dkit: unknown command '#{command}'. Run 'dkit help'."
    exit 1
  end
end