diff --git a/lib/portainer_cli/commands/console.rb b/lib/portainer_cli/commands/console.rb index 0e2ae43..24ac4b4 100644 --- a/lib/portainer_cli/commands/console.rb +++ b/lib/portainer_cli/commands/console.rb @@ -32,7 +32,7 @@ module PortainerCli $stderr.puts dim("Stack: #{stack['Name']} Endpoint: #{endpoint_id}") - container_id = resolve_container(endpoint_id, container_name) + container_id = resolve_container(endpoint_id, container_name, stack['Name']) $stderr.puts dim("Connecting to #{container_name} (#{container_id[0, 12]})...") @@ -65,24 +65,36 @@ module PortainerCli match end - def resolve_container(endpoint_id, name_or_id) + def resolve_container(endpoint_id, name_or_id, stack_name = nil) containers = @client.get("/api/endpoints/#{endpoint_id}/docker/containers/json", all: 1) + names_for = ->(c) { Array(c['Names']).map { |n| n.delete_prefix('/') } } # 1. Exact match: full name or ID prefix exact = containers.find do |c| c['Id'].start_with?(name_or_id) || - Array(c['Names']).any? { |n| n.delete_prefix('/') == name_or_id } + names_for.(c).any? { |n| n == name_or_id } end return exact['Id'] if exact - # 2. Fuzzy: container name contains the search term as a word segment - # Prefers "--" pattern, falls back to any substring match - names_for = ->(c) { Array(c['Names']).map { |n| n.delete_prefix('/') } } - - fuzzy = containers.select do |c| - names_for.(c).any? { |n| n.include?(name_or_id) } + # 2. Compose-style match: "--" + # This is the most precise fuzzy match when we know the stack name. + if stack_name + composed = containers.select do |c| + names_for.(c).any? { |n| n == "#{stack_name}-#{name_or_id}-1" || n.match?(/\A#{Regexp.escape(stack_name)}-#{Regexp.escape(name_or_id)}-\d+\z/) } + end + if composed.size == 1 + matched_name = names_for.(composed.first).first + $stderr.puts dim("Matched '#{name_or_id}' → #{matched_name}") + return composed.first['Id'] + end end + # 3. Substring fallback (only within this stack's containers) + stack_prefix = stack_name ? "#{stack_name}-" : nil + pool = stack_prefix ? containers.select { |c| names_for.(c).any? { |n| n.start_with?(stack_prefix) } } : containers + + fuzzy = pool.select { |c| names_for.(c).any? { |n| n.include?(name_or_id) } } + if fuzzy.size == 1 matched_name = names_for.(fuzzy.first).first $stderr.puts dim("Matched '#{name_or_id}' → #{matched_name}") @@ -94,8 +106,8 @@ module PortainerCli error("Ambiguous container '#{name_or_id}'. Did you mean one of:\n#{list}") end - available = containers.map { |c| " #{c['Id'][0, 12]} #{names_for.(c).first}" }.join("\n") - error("Container '#{name_or_id}' not found on endpoint #{endpoint_id}. Available:\n#{available}") + available = pool.map { |c| " #{c['Id'][0, 12]} #{names_for.(c).first}" }.join("\n") + error("Container '#{name_or_id}' not found. Available:\n#{available}") end def create_exec(endpoint_id, container_id, shell)