diff --git a/.tool-versions b/.tool-versions index 4991363..6a1399d 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -crystal 0.32.1 +crystal 0.35.1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f1884d9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM crystallang/crystal:0.35.1-alpine +RUN apk add --update --no-cache --force-overwrite \ +openssl-libs-static openssl-dev g++ gc-dev \ +libc-dev libevent-dev libevent-static libxml2-dev llvm llvm-dev \ +llvm-static make pcre-dev readline-dev readline-static \ +yaml-dev zlib-dev zlib-static ncurses-static sqlite-dev sqlite-static openssl-dev +#ncurses-libs ncurses-dev + diff --git a/README.md b/README.md index 538970a..185cf94 100644 --- a/README.md +++ b/README.md @@ -26,20 +26,20 @@ sudo snap install snipcli --beta ### From source -Snipline CLI requires Crystal 0.30.1 to be installed to install from source +Snipline CLI requires Crystal 0.35.1 to be installed to install from source ```bash # Clone the repo git clone git@github.com:snipline/snipcli.git # Checkout the latest release -git checkout +git checkout # Make sure you have the same Crystal installed that's required in shard.yml crystal -v # Install dependencies shards -# Build app for Crystal 0.32.1 / MacOS +# Build app for Crystal 0.34.0 / MacOS crystal build src/snipline_cli.cr -o snipcli --release -o snipcli -# Build app for Crystal 0.31.1 / Linux (Alpine) +# Build app for Crystal 0.34.0 / Linux (Alpine) crystal build src/snipline_cli.cr -o snipcli --release -o snipcli -Dstatic_linux ./snipcli --version ``` @@ -128,11 +128,12 @@ As of 0.3.0 the web interface has been removed infavour of the new TUI. See the Installation section on building from source. -Set log levels for additional development output. +Set log levels for additional development output and specifiy different config files for testing ```bash crystal build src/snipline_cli.cr -o snipcli -env LOG_LEVEL=DEBUG ./snipcli search git +env CONFIG_FILE=~/.config/snipline/config.dev.toml ./snipcli search login +env CONFIG_FILE=~/.config/snipline/config.dev.toml env CRYSTAL_LOG_LEVEL=INFO ./snipcli search git ``` To change the config file location (For testing) use the `CONFIG_FILE` environment variable. diff --git a/shard.lock b/shard.lock index a1ec933..aa27eea 100644 --- a/shard.lock +++ b/shard.lock @@ -1,42 +1,46 @@ -version: 1.0 +version: 2.0 shards: admiral: - github: jwaldrip/admiral.cr - version: 1.9.0 + git: https://github.com/jwaldrip/admiral.cr.git + version: 1.11.2 ameba: - github: veelenga/ameba - version: 0.11.0 + git: https://github.com/veelenga/ameba.git + version: 0.13.2 crecto: - github: Crecto/crecto - version: 0.11.2 + git: https://github.com/Crecto/crecto.git + version: 0.11.3 crest: - github: mamantoha/crest - version: 0.22.0 + git: https://github.com/mamantoha/crest.git + version: 0.26.1 db: - github: crystal-lang/crystal-db - version: 0.7.0 + git: https://github.com/crystal-lang/crystal-db.git + version: 0.9.0 fuzzy_match: - github: acoustep/fuzzy_match.cr - commit: fe9eed429118adc8f0f9f6c91de4f11cdca5840e + git: https://github.com/acoustep/fuzzy_match.cr.git + version: 0.3.0+git.commit.3834b23deab80c4554ce15b015b804f010e2b768 http-client-digest_auth: - github: mamantoha/http-client-digest_auth - version: 0.3.0 + git: https://github.com/mamantoha/http-client-digest_auth.git + version: 0.4.0 + + http_proxy: + git: https://github.com/mamantoha/http_proxy.git + version: 0.7.1 ncurses: - github: JohnDowson/ncurses - commit: d0e46f673b6a443255269ce610d598a0fdc7d159 + git: https://github.com/JohnDowson/ncurses.git + version: 0.0.3+git.commit.d0e46f673b6a443255269ce610d598a0fdc7d159 sqlite3: - github: crystal-lang/crystal-sqlite3 - version: 0.14.0 + git: https://github.com/crystal-lang/crystal-sqlite3.git + version: 0.16.0 toml: - github: crystal-community/toml.cr - commit: d02de85eed68a70dc97ab6d6e52831a4d0e890fe + git: https://github.com/crystal-community/toml.cr.git + version: 0.6.1+git.commit.d02de85eed68a70dc97ab6d6e52831a4d0e890fe diff --git a/shard.yml b/shard.yml index 8774805..0b4874d 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: cli -version: 0.3.3 +version: 0.4.0 authors: - Mitchell Stanley @@ -8,7 +8,7 @@ targets: cli2: main: src/cli.cr -crystal: < 0.33 +crystal: < 0.36.0 license: MIT @@ -17,7 +17,7 @@ dependencies: github: jwaldrip/admiral.cr crest: github: mamantoha/crest - version: ~> 0.22.0 + version: ~> 0.26.1 toml: github: crystal-community/toml.cr branch: master @@ -26,9 +26,10 @@ dependencies: branch: borders_and_lines crecto: github: Crecto/crecto + version: ~> 0.11.3 sqlite3: github: crystal-lang/crystal-sqlite3 - version: ~> 0.14.0 + version: ~> 0.16.0 fuzzy_match: github: acoustep/fuzzy_match.cr branch: master @@ -38,4 +39,4 @@ dependencies: development_dependencies: ameba: github: veelenga/ameba - version: ~> 0.11.0 + version: ~> 0.13.2 diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index a648022..e6864f7 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: snipcli -version: 0.3.3 +version: 0.4.0 summary: Shell Snippet organiser description: > Snipcli is a commandline interface for managing shell commands. Sync commands with your Snipline account or use in guest mode. Snipline lets you dynamically change command parameters easily so you never have to remember how to build a command. diff --git a/src/snipline_cli.cr b/src/snipline_cli.cr index 82f90bf..a506f46 100644 --- a/src/snipline_cli.cr +++ b/src/snipline_cli.cr @@ -5,6 +5,7 @@ require "json" require "ncurses" require "sqlite3" require "crecto" +require "log" require "./snipline_cli/config" require "./snipline_cli/helpers/*" @@ -17,6 +18,8 @@ require "./snipline_cli/commands/*" include SniplineCli::Services +Log.setup_from_env + module Repo extend Crecto::Repo @@ -27,7 +30,7 @@ module Repo end module SniplineCli - VERSION = "0.3.3" + VERSION = "0.4.0" def self.config Config.config @@ -37,10 +40,6 @@ module SniplineCli ENV.has_key?("CONFIG_FILE") ? ENV["CONFIG_FILE"] : "~/.config/snipline/config.toml" end - def self.log - Log.log - end - # The base Command Class that inherits from [Admiral](https://github.com/jwaldrip/admiral.cr) # # This command is not used by itself diff --git a/src/snipline_cli/commands/login.cr b/src/snipline_cli/commands/login.cr index e341a47..b426633 100644 --- a/src/snipline_cli/commands/login.cr +++ b/src/snipline_cli/commands/login.cr @@ -14,41 +14,52 @@ module SniplineCli def run config = SniplineCli.config - log = SniplineCli.log puts "What's your Snipline email account?".colorize.mode(:bold) puts "Register at #{"https://account.snipline.io/register".colorize.mode(:underline)} if you don't have an account." + Log.debug { "Test debug" } print "Email:" email = gets - spawn do - Crest.post( - "#{config.get("api.url")}/sessions", - form: {:email => email} - ) - end - puts "Thanks, we're sending you a verification code..." - sleep 1 - puts "Please enter the verification code that was sent to your email:" - print "Verification Code:" - verification_code = gets - log.debug("verification_code: #{verification_code}") - if verification_code.nil? || verification_code.empty? - puts "Code not entered. Please try again." + # spawn do + # Crest.post( + # "#{config.get("api.url")}/sessions", + # form: {:email => email} + # ) + # end + print "Enter your Snipline Account password:" + # password = gets + password = STDIN.noecho &.gets.try &.chomp + password = password.as?(String) || "" + + Log.debug { + + password_length = password.size + hidden_password = password.chars.map_with_index { |c, i| + if i == 0 || i == (password_length - 1) + c + else + '*' + end + }.to_s + + "password: #{hidden_password}" + } + if password.empty? + puts "Password not entered. Please try again." return end + puts "" puts "One moment..." - puts "URL: #{config.get("api.url")}" begin Crest.post( - "#{config.get("api.url")}/tokens/create", + "#{config.get("api.url")}/v2/sessions", form: { - :id => email, - :token => verification_code, - :length => "year", + :email => email, + :password => password } ) do |response| - log.debug("response body") + Log.debug { "response body" } json_string = response.body_io.gets_to_end - log.debug(json_string.inspect) + Log.debug { json_string.inspect } token = Token.from_json(json_string) toml_contents = <<-TOML title = "Snipline" @@ -68,13 +79,12 @@ module SniplineCli end rescue ex : Crest::NotFound puts "404 Not Found :(" - ex.response rescue ex : Crest::InternalServerError puts "Internal server error" - ex.response rescue ex : Crest::Forbidden - puts "Incorrect Token" - ex.response + puts "Incorrect password" + rescue + puts "Unknown error" end end end diff --git a/src/snipline_cli/models/snippet.cr b/src/snipline_cli/models/snippet.cr index 9783127..7c5ed7f 100644 --- a/src/snipline_cli/models/snippet.cr +++ b/src/snipline_cli/models/snippet.cr @@ -33,7 +33,7 @@ module SniplineCli::Models param_name = split_equals.shift unparsed_params = split_equals.join("=") if unparsed_params.is_a?(String) - options = unparsed_params.split(",").map(&.strip) + options = unparsed_params.gsub("\\,", "###COMMA###").split(",").map(&.gsub("###COMMA###", ",")).map(&.strip) # todo if options.is_a?(Array(String)) options = options.as(Array(String)) diff --git a/src/snipline_cli/parsers/snippet_data_parser.cr b/src/snipline_cli/parsers/snippet_data_parser.cr index ade32aa..f22f53d 100644 --- a/src/snipline_cli/parsers/snippet_data_parser.cr +++ b/src/snipline_cli/parsers/snippet_data_parser.cr @@ -1,28 +1,36 @@ module SniplineCli::Parsers class SnippetDataParser - JSON.mapping({ - data: Array(SnippetParser), - }) + include JSON::Serializable + + @[JSON::Field(key: "data")] + property data : Array(SnippetParser) end class SingleSnippetDataParser - JSON.mapping({ - data: SnippetParser, - }) + include JSON::Serializable + + @[JSON::Field(key: "data")] + property data : SnippetParser end class SnippetError - JSON.mapping({ - detail: String, - source: Hash(String, String), - title: String, - }) + include JSON::Serializable + + @[JSON::Field(key: "detail")] + property detail : String + + @[JSON::Field(key: "source")] + property source : Hash(String, String) + + @[JSON::Field(key: "title")] + property title : String end class SnippetErrorResponse - JSON.mapping({ - errors: Array(SnippetError), - }) + include JSON::Serializable + + @[JSON::Field(key: "errors")] + property errors : Array(SnippetError) def has_key?(key) @errors.any? { |error| diff --git a/src/snipline_cli/parsers/snippet_parser.cr b/src/snipline_cli/parsers/snippet_parser.cr index 9753a8e..d93a5e5 100644 --- a/src/snipline_cli/parsers/snippet_parser.cr +++ b/src/snipline_cli/parsers/snippet_parser.cr @@ -5,11 +5,22 @@ module SniplineCli::Parsers # # Note how most attributes are contained within the `attribute` attribute. This is to conform to the JSON-API specification. class SnippetParser - JSON.mapping({ - id: String | Nil, - type: String, - attributes: SnippetAttributeParser, - }) + include JSON::Serializable + + @[JSON::Field(key: "id")] + property id : String | Nil + + @[JSON::Field(key: "type")] + property type : String + + @[JSON::Field(key: "attributes")] + property attributes : SnippetAttributeParser + + # JSON.mapping({ + # id: String | Nil, + # type: String, + # attributes: SnippetAttributeParser, + # }) def initialize(@id : String | Nil, @type : String, @attributes : SnippetAttributeParser) end diff --git a/src/snipline_cli/services/create_config_directory.cr b/src/snipline_cli/services/create_config_directory.cr index 7b4626c..dbbdbc3 100644 --- a/src/snipline_cli/services/create_config_directory.cr +++ b/src/snipline_cli/services/create_config_directory.cr @@ -1,12 +1,14 @@ require "../helpers/*" +require "log" + module SniplineCli module Services # Creates a config directory for storing configuration files for SnipCLI. class CreateConfigDirectory def self.run(file) - directory_name = SniplineCli::Helpers.expand_path(File.dirname(file)) + directory_name = SniplineCli::Helpers.expand_path(File.dirname(file)) unless File.directory?(directory_name) - SniplineCli.log.debug("Making config directory #{directory_name}") + Log.debug { "Making config directory #{directory_name}" } Dir.mkdir(directory_name) end end diff --git a/src/snipline_cli/services/display_results.cr b/src/snipline_cli/services/display_results.cr index b598469..77b4937 100644 --- a/src/snipline_cli/services/display_results.cr +++ b/src/snipline_cli/services/display_results.cr @@ -35,6 +35,7 @@ module SniplineCli::Services break if codepoint == 17 # C+q - quit break if run_character_key(ch, codepoint) == false @left_pane.filter(@search.search_text) + else end @search.window.refresh end @@ -54,19 +55,27 @@ module SniplineCli::Services @left_pane.select_lower refresh_right_pane elsif codepoint == 67 || codepoint == 10 # Shift+c / Enter - copy - output = build_snippet - copy_snippet(output) - return false + unless @left_pane.results.size <= 0 + output = build_snippet + copy_snippet(output) + return false + end elsif codepoint == 68 # Shift+d - delete - delete_snippet - return false + unless @left_pane.results.size <= 0 + delete_snippet + return false + end elsif codepoint == 69 # Shift+e - edit - edit_snippet - return false + unless @left_pane.results.size <= 0 + edit_snippet + return false + end elsif codepoint == 82 # Shift+r - run - output = build_snippet - run_snippet(output) - return false + unless @left_pane.results.size <= 0 + output = build_snippet + run_snippet(output) + return false + end else @search.write(ch) @left_pane.filter(@search.search_text) diff --git a/src/snipline_cli/services/edit_snippet.cr b/src/snipline_cli/services/edit_snippet.cr index 320a582..cf60697 100644 --- a/src/snipline_cli/services/edit_snippet.cr +++ b/src/snipline_cli/services/edit_snippet.cr @@ -1,4 +1,5 @@ require "../helpers/*" +require "log" module SniplineCli module Services @@ -10,8 +11,7 @@ module SniplineCli class EditSnippet def self.run(snippet : Snippet, input, output) config = SniplineCli.config - log = SniplineCli.log - log.info("editing snippet #{snippet.name}") + Log.info { "editing snippet #{snippet.name}" } temp_file = TempSnippetEditorFile.new(snippet) temp_file.create loop do @@ -19,8 +19,8 @@ module SniplineCli snippet_attributes = temp_file.read snippet.name = snippet_attributes.name snippet.real_command = snippet_attributes.real_command.strip - log.info(snippet.real_command.not_nil!) - log.info(snippet_attributes.real_command.not_nil!) + Log.info { snippet.real_command.not_nil! } + Log.info { snippet_attributes.real_command.not_nil! } snippet.documentation = snippet_attributes.documentation snippet.tags = (snippet_attributes.tags.nil?) ? nil : snippet_attributes.tags.not_nil!.join(",") snippet.snippet_alias = snippet_attributes.snippet_alias diff --git a/src/snipline_cli/services/load_snippets.cr b/src/snipline_cli/services/load_snippets.cr index 2486fde..21f6263 100644 --- a/src/snipline_cli/services/load_snippets.cr +++ b/src/snipline_cli/services/load_snippets.cr @@ -12,10 +12,9 @@ module SniplineCli class LoadSnippets def self.run : Array(Snippet) config = SniplineCli.config - log = SniplineCli.log - log.info("Looking through file #{config.get("general.db")}") + Log.debug { "Looking through file #{config.get("general.db")}" } unless File.readable?(expand_path(config.get("general.db"))) - log.warn("Could not read #{config.get("general.db")}") + Log.warn { "Could not read #{config.get("general.db")}" } abort("Run #{"snipline-cli sync".colorize(:green)} first") end # File.open(File.expand_path(config.get("general.db"))) do |file| diff --git a/src/snipline_cli/services/log.cr b/src/snipline_cli/services/log.cr deleted file mode 100644 index ffadc3f..0000000 --- a/src/snipline_cli/services/log.cr +++ /dev/null @@ -1,47 +0,0 @@ -require "logger" - -module SniplineCli - module Services - # Log is a wrapper around the Crystal `Logger`. - # - # Example Usage - # - # ```crystal - # log = SniplineCli.log - # config = SniplineCli.config - # log.info("Looking through file #{config.get("general.file")}") - # ``` - # - # The output level can then be specified at run-time. - # - # ```bash - # env LOG_LEVEL=WARN snipcli sync - # ``` - class Log - # Constant that creates a fresh version of itself - for use with self.log. - INSTANCE = Log.new - - property logger : Logger - - def initialize - ENV["LOG_LEVEL"] ||= "WARN" - @logger = Logger.new(STDOUT, level: (ENV["LOG_LEVEL"] == "DEBUG") ? Logger::DEBUG : Logger::WARN) - end - - # The static convenience method for retreiving the Log class instance without regenerating it. - def self.log - Log::INSTANCE - end - - macro define_methods(names) - {% for name in names %} - def {{name}}(message : String) - @logger.{{name}}(message) - end - {% end %} - end - - define_methods([debug, warn, info, fatal]) - end - end -end diff --git a/src/snipline_cli/services/snipline_api.cr b/src/snipline_cli/services/snipline_api.cr index e2386d1..25b6b37 100644 --- a/src/snipline_cli/services/snipline_api.cr +++ b/src/snipline_cli/services/snipline_api.cr @@ -37,7 +37,7 @@ module SniplineCli::Services # :tags => snippet.tags # } }, - logging: ENV["LOG_LEVEL"] == "DEBUG" ? true : false + logging: ENV["CRYSTAL_LOG_LEVEL"] == "DEBUG" ? true : false ) SingleSnippetDataParser.from_json(resp.body).data end @@ -61,7 +61,7 @@ module SniplineCli::Services # :tags => snippet.tags # } }, - logging: ENV["LOG_LEVEL"] == "DEBUG" ? true : false + logging: ENV["CRYSTAL_LOG_LEVEL"] == "DEBUG" ? true : false ) response = SingleSnippetDataParser.from_json(resp.body).data snippet.name = response.name.not_nil! @@ -90,7 +90,7 @@ module SniplineCli::Services # "Accept" => "application/vnd.api+json", "Authorization" => "Bearer #{config.get("api.token")}", }, - logging: ENV["LOG_LEVEL"] == "DEBUG" ? true : false + logging: ENV["CRYSTAL_LOG_LEVEL"] == "DEBUG" ? true : false ) true rescue ex