devcenter 1.0.7 → 1.1.0rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
- metadata.gz: f8cf248735847c0a751bf6947c723d2dcff7245a
4
- data.tar.gz: a4c53ace9bb2948333ba95fee47651129b05c5fa
3
+ metadata.gz: 592c9194f39d3e37bf58ec40370e83df3f157c14
4
+ data.tar.gz: d2f34c3bdf1ac94219fd567a821e01b98306aeff
5
5
  !binary "U0hBNTEy":
6
- metadata.gz: 4cc8bf967f1c7c381bf33417f413a95982bd92b6b6985d9d8749f1b22204af885cb117fd23f0be240495e8d4ad9736facd62b8a96191696cd82f70692ea207e8
7
- data.tar.gz: a7705b6253fb6dade4c4a0739b941485005557c2ba56029dd0a41c49d0ecae98241f0e60153081aaee9c52ab57e39d222a5225353d5ad1a6b3e065d1b2201650
6
+ metadata.gz: f7ec004380727c6a826b2a8efab3610b5704734fdbb09054b3419c315eac0aa7fe6d7cf5a8a88078afbf722cfd5b09642edba40325dd4e7988b118ace13e12f2
7
+ data.tar.gz: 001e0ec246f33908a0632579efb6f55d20905d2a74558ca4bef740a6d514d264fac6c94a70433d7e5618a75139d80082f1c5a5e785124b27a4d73f67e19e9691
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Devcenter
1
+ # Dev Center CLI
2
2
 
3
3
  CLI to interact with Heroku's Dev Center
4
4
 
@@ -30,6 +30,12 @@ This will save an `article-slug.md` text file in your local directory. The file
30
30
 
31
31
  This will open a preview in your default browser and get it refreshed when you save the file. You can specify `--port` and `--host` options to customize the preview web server.
32
32
 
33
+ ### Update an article in Dev Center from a local file
34
+
35
+ $ devcenter push dynos
36
+
37
+ This will save the title and content from your local article in Dev Center. You'll be asked to authenticate to verify that you have permissions to update the article.
38
+
33
39
  ### Help
34
40
 
35
41
  Get available commands
@@ -48,4 +54,4 @@ If you have a Dev Center instance, you can point your CLI to it by setting the `
48
54
 
49
55
  See LICENSE.txt file.
50
56
 
51
- The `preview` command uses the [Font Awesome](http://fontawesome.io/) vector icons, which have their own [License](https://github.com/FortAwesome/Font-Awesome#license).
57
+ The `preview` command uses the [Font Awesome](http://fontawesome.io/) vector icons, which have their own [License](https://github.com/FortAwesome/Font-Awesome#license).
data/lib/devcenter.rb CHANGED
@@ -4,5 +4,6 @@ require_relative 'devcenter/logger'
4
4
  require_relative 'devcenter/helpers'
5
5
  require_relative 'devcenter/client'
6
6
  require_relative 'devcenter/coderay_extensions'
7
+ require_relative 'devcenter/article_file'
7
8
  require_relative 'devcenter/previewer'
8
9
  require_relative 'devcenter/cli'
@@ -0,0 +1,32 @@
1
+ require 'devcenter-parser'
2
+
3
+ module Devcenter
4
+
5
+ class ArticleFile
6
+
7
+ attr_reader :metadata, :html, :content, :parsing_error, :toc
8
+
9
+ def initialize(opts = {})
10
+ @metadata = opts[:metadata] || OpenStruct.new
11
+ @content = opts[:content] || ''
12
+ markdown_flavour = @metadata.markdown_flavour || :maruku
13
+ begin
14
+ @html = ::DevcenterParser.to_html(@content, markdown_flavour.to_sym)
15
+ rescue Exception => e
16
+ @parsing_error = e.to_s
17
+ @html = ''
18
+ end
19
+ @toc = Nokogiri::HTML(@html).search('h2')
20
+ end
21
+
22
+ def self.read(src_path)
23
+ src = IO.read(src_path)
24
+ metadata_yaml, content = src.split(/\r*\n\r*\n/, 2)
25
+ metadata = OpenStruct.new YAML.load(metadata_yaml)
26
+ markdown_flavour = metadata.markdown_flavour || :maruku
27
+ new(metadata: metadata, content: content)
28
+ end
29
+
30
+ end
31
+
32
+ end
data/lib/devcenter/cli.rb CHANGED
@@ -36,6 +36,19 @@ command :pull do |c|
36
36
  end
37
37
  end
38
38
 
39
+ command :push do |c|
40
+ c.syntax = 'devcenter push [options]'
41
+ c.summary = 'Submit the content of a local article to Dev Center'
42
+ c.description = c.summary
43
+ c.example 'devcenter push process-model.md', 'Submits the content of the local "process-model.md" file to the article with the "process-model" slug in Dev Center'
44
+ c.option '--debug', 'Output internal log to help debugging'
45
+ c.action do |args, options|
46
+ options.default :debug => false
47
+ Devcenter::Logger.active = options.debug
48
+ Devcenter::Commands::Push.run(args[0], options.force)
49
+ end
50
+ end
51
+
39
52
  command :preview do |c|
40
53
  c.syntax = 'devcenter preview [options]'
41
54
  c.summary = 'Opens a live preview for a given article file'
@@ -1,22 +1,97 @@
1
1
  require 'singleton'
2
2
  require 'excon'
3
+ require 'base64'
3
4
 
4
5
  module Devcenter
5
6
 
6
7
  module Client
8
+
9
+ class Response
10
+ def initialize(response)
11
+ @status, @body, @headers = response.status, response.body, response.headers
12
+ end
13
+
14
+ def ok?
15
+ [200, 201].include?(@status)
16
+ end
17
+
18
+ def redirect?
19
+ [301, 302].include?(@status)
20
+ end
21
+
22
+ def not_found?
23
+ @status == 404
24
+ end
25
+
26
+ def access_denied?
27
+ @status == 401
28
+ end
29
+
30
+ def body
31
+ JSON.parse(@body)
32
+ end
33
+
34
+ def location
35
+ @headers['Location']
36
+ end
37
+ end
38
+
7
39
  include Devcenter::Helpers
8
40
  extend self
9
41
 
42
+ def validate_article(token, article_id, article_params)
43
+ form = URI.encode_www_form(article_params)
44
+ auth_request(:post, token, path: validate_article_path(article_id), body: form, :headers => { "Content-Type" => "application/x-www-form-urlencoded" })
45
+ end
46
+
47
+ def update_article(token, article_id, article_params)
48
+ form = URI.encode_www_form(article_params)
49
+ auth_request(:put, token, path: update_article_path(article_id), body: form, :headers => { "Content-Type" => "application/x-www-form-urlencoded" })
50
+ end
51
+
52
+ def response(request_call)
53
+ Response.new request_call
54
+ end
55
+
10
56
  def head(args)
11
- client.head(args)
57
+ response client.head(args)
12
58
  end
13
59
 
14
60
  def get(args)
15
- client.get(args)
61
+ response client.get(args)
62
+ end
63
+
64
+ def put(args)
65
+ response client.put(args)
66
+ end
67
+
68
+ def auth_request(method, token, args)
69
+ args[:headers] ||= {}
70
+ args[:headers].merge!(default_headers)
71
+ args[:headers]["Authorization"] = "Basic #{encode64(token)}"
72
+ response client.send(method, args)
16
73
  end
17
74
 
18
75
  def client
19
- @client ||= Excon.new(devcenter_base_url, :headers => { 'User-agent' => 'DevCenterCLI'})
76
+ @client ||= Excon.new(devcenter_base_url, :headers => default_headers)
77
+ end
78
+
79
+ def default_headers
80
+ {'User-agent' => 'DevCenterCLI'}
81
+ end
82
+
83
+ def get_oauth_token(email, password)
84
+ auth = encode64("#{email}:#{password}")
85
+ response Excon.post('https://api.heroku.com/oauth/authorizations',
86
+ headers: {
87
+ "Content-Type" => "application/json",
88
+ "Accept" => "application/vnd.heroku+json; version=3",
89
+ "Authorization" => "Basic #{auth}"
90
+ })
91
+ end
92
+
93
+ def encode64(str)
94
+ Base64.encode64(str).strip
20
95
  end
21
96
  end
22
97
 
@@ -1,4 +1,5 @@
1
1
  require_relative 'commands/base'
2
2
  require_relative 'commands/open'
3
3
  require_relative 'commands/pull'
4
+ require_relative 'commands/push'
4
5
  require_relative 'commands/preview'
@@ -24,6 +24,7 @@ module Devcenter::Commands
24
24
  validate
25
25
  if @validation_errors.any?
26
26
  @validation_errors.each{ |e| say e }
27
+ abort
27
28
  else
28
29
  run
29
30
  end
@@ -36,7 +37,8 @@ module Devcenter::Commands
36
37
 
37
38
  def article_not_found!(slug)
38
39
  message = ["No #{slug} article found."]
39
- suggestions = JSON.parse(Devcenter::Client.get(:path => search_api_path, :query => { :q => slug, :source => 'devcenter-cli' }).body)['devcenter']
40
+ response = Devcenter::Client.get(:path => search_api_path, :query => { :q => slug, :source => 'devcenter-cli' })
41
+ suggestions = response.body['devcenter']
40
42
  suggestions.select!{ |s| article_url?(s['full_url']) }
41
43
  suggestions.each{ |s| s['slug'] = slug_from_article_url(s['full_url']) }
42
44
  unless suggestions.empty?
@@ -46,8 +48,7 @@ module Devcenter::Commands
46
48
  message << " %-#{longest}s # %s" % [suggestion['slug'], suggestion['title']]
47
49
  end
48
50
  end
49
- say message.join("\n")
50
- exit
51
+ abort message.join("\n")
51
52
  end
52
53
 
53
54
  end
@@ -16,15 +16,14 @@ module Devcenter::Commands
16
16
  def run
17
17
  path = article_path(@slug)
18
18
  log "Connecting to #{path}"
19
- head = Devcenter::Client.head(:path => path)
20
- case head.status
21
- when 200
19
+ response = Devcenter::Client.head(:path => path)
20
+ if response.ok?
22
21
  log "Page found, opening"
23
22
  launchy = Launchy.open(devcenter_base_url + path)
24
23
  launchy.join if launchy.respond_to?(:join)
25
- when 301, 302
26
- say "Redirected to #{head.headers['Location']}"
27
- when 404
24
+ elsif response.redirect?
25
+ abort "Redirected to #{response.location}"
26
+ elsif response.not_found?
28
27
  article_not_found!(@slug)
29
28
  end
30
29
  end
@@ -16,10 +16,10 @@ module Devcenter::Commands
16
16
 
17
17
  def run
18
18
  response = Devcenter::Client.get(path: article_api_path(@slug))
19
- article_received = response.status == 200 && JSON.parse(response.body)['article'] && JSON.parse(response.body)['article']['id']
19
+ article_received = response.ok? && response.body['article'] && response.body['article']['id']
20
20
  article_not_found!(@slug) unless article_received
21
21
 
22
- article = JSON.parse(response.body)['article']
22
+ article = response.body['article']
23
23
  metadata = {'title' => article['title'], 'id' => article['id'], 'markdown_flavour' => article['markdown_flavour']}
24
24
  file_path = md_file_path(@slug)
25
25
 
@@ -0,0 +1,74 @@
1
+ module Devcenter::Commands
2
+
3
+ class Push < Base
4
+
5
+ def initialize(*args)
6
+ @slug = args[0].to_s.gsub(/.md\z/, '') # maybe the user provides the filename by mistake
7
+ @md_path = md_file_path(@slug)
8
+ super
9
+ end
10
+
11
+ def validate
12
+ empty_slug = @slug.nil? || @slug.to_s.strip.empty?
13
+ file_exists = !empty_slug && File.exists?(@md_path)
14
+ if empty_slug
15
+ @validation_errors << 'Please provide an article slug'
16
+ elsif !file_exists
17
+ @validation_errors << "Can't find #{@md_path} file - you may want to `devcenter pull #{@slug}`"
18
+ end
19
+ end
20
+
21
+ def run
22
+ @article = ::Devcenter::ArticleFile.read(@md_path)
23
+ if @article.parsing_error
24
+ abort "The content of #{@md_path} can't be parsed properly, fix it and try again."
25
+ end
26
+ say 'Authenticate with your Heroku account:'
27
+ email = ask('Email: ')
28
+ password = ask('Password: ') { |q| q.echo = '*' }
29
+ response = Devcenter::Client.get_oauth_token(email, password)
30
+ if response.access_denied?
31
+ abort "Authentication error: bad credentials. Please try again."
32
+ elsif response.ok?
33
+ token = response.body['access_token']['token']
34
+ push_article(token) if validate_article(token)
35
+ else
36
+ abort "Authentication error: #{response.body['message']}"
37
+ end
38
+ end
39
+
40
+ def validate_article(token)
41
+ article_id = @article.metadata.id
42
+ article_params = {
43
+ 'article[content]' => @article.content,
44
+ 'article[title]' => @article.metadata.title,
45
+ 'article[parser]' => @article.metadata.parser || @article.metadata.markdown_flavour
46
+ }
47
+ response = Devcenter::Client.validate_article(token, article_id, article_params)
48
+ errors = response.body
49
+ if errors.any?
50
+ say "The article \"#{@slug}\" is not valid:"
51
+ abort errors.to_yaml.gsub(/\A(\-+)\n-/, '')
52
+ else
53
+ say "The article \"#{@slug}\" is valid."
54
+ end
55
+ errors.empty?
56
+ end
57
+
58
+ def push_article(token)
59
+ article_id = @article.metadata.id
60
+ article_params = {
61
+ 'article[content]' => @article.content,
62
+ 'article[title]' => @article.metadata.title,
63
+ 'article[parser]' => @article.metadata.parser || @article.metadata.markdown_flavour
64
+ }
65
+ response = Devcenter::Client.update_article(token, article_id, article_params)
66
+ if response.ok?
67
+ say "\"#{@slug}\" pushed successfully."
68
+ else
69
+ error = response.body['error']
70
+ abort "Error pushing \"#{@slug}\": #{error}"
71
+ end
72
+ end
73
+ end
74
+ end
@@ -16,6 +16,14 @@ module Devcenter::Helpers
16
16
  "/articles.json"
17
17
  end
18
18
 
19
+ def validate_article_path(id)
20
+ "/api/v1/private/articles/#{id}/validate.json"
21
+ end
22
+
23
+ def update_article_path(id)
24
+ "/api/v1/private/articles/#{id}.json"
25
+ end
26
+
19
27
  def article_url?(url)
20
28
  escaped_base_url = devcenter_base_url.gsub('/','\\/')
21
29
  url.match(/\A#{escaped_base_url}\/articles\/.+/)
@@ -1,6 +1,6 @@
1
1
  <section class="main">
2
2
 
3
- <article class="article autolink <% 'contributed' if @article.external_owner? %>">
3
+ <article class="article autolink">
4
4
 
5
5
  <header>
6
6
  <h1 class="doc"><%= @article.metadata.title %></h1>
@@ -23,24 +23,14 @@
23
23
  </section>
24
24
 
25
25
  <%= @article.html %>
26
- <% elsif @article.error %>
26
+ <% elsif @article.parsing_error %>
27
27
  <div class="cli-preview-error">
28
28
  <h4>Parsing error:</h4>
29
29
  <p>
30
- <%= @article.error %>
30
+ <%= @article.parsing_error %>
31
31
  </p>
32
32
  </div>
33
33
  <% end %>
34
-
35
- <form class="cli-save" action="<%= devcenter_base_url %>/admin/articles/cli-edit/<%= @article.metadata.id %>" method="POST">
36
- <input name="_method" type="hidden" value="put">
37
- <input type="hidden" name="article[title]" value="<%= @article.metadata.title %>">
38
- <input type="hidden" name="article[markdown_flavour]" value="<%= @article.metadata.markdown_flavour %>">
39
- <textarea style="display:none" name="article[content]">
40
- <%= CGI.escapeHTML(@article.content.to_s.lstrip) %>
41
- </textarea>
42
- <input type="submit" value="Save in Dev Center">
43
- </form>
44
34
  </div>
45
35
 
46
36
  </article>
@@ -1,6 +1,5 @@
1
1
  require 'sinatra'
2
2
  require 'rack/highlighter'
3
- require 'devcenter-parser'
4
3
 
5
4
  module Devcenter::Previewer
6
5
 
@@ -51,8 +50,7 @@ module Devcenter::Previewer
51
50
  src_path = File.join(Dir.pwd, "#{params[:slug]}.md")
52
51
  if File.exists?(src_path)
53
52
  log "Parsing"
54
- @article = parse_article(src_path)
55
- @article.toc = Nokogiri::HTML(@article.html).search('h2')
53
+ @article = Devcenter::ArticleFile.read(src_path)
56
54
  @page_title = @article.metadata.title
57
55
  log "Serving"
58
56
  erb :article
@@ -63,20 +61,6 @@ module Devcenter::Previewer
63
61
  end
64
62
  end
65
63
 
66
- def parse_article(src_path)
67
- article = OpenStruct.new
68
- src = IO.read(src_path)
69
- metadata_yaml, article.content = src.split(/\r*\n\r*\n/, 2)
70
- article.metadata = OpenStruct.new YAML.load(metadata_yaml)
71
- markdown_flavour = article.metadata.markdown_flavour || :maruku
72
- begin
73
- article.html = ::DevcenterParser.to_html(article.content, markdown_flavour.to_sym)
74
- rescue Exception => e
75
- article.error = e.to_s
76
- end
77
- article
78
- end
79
-
80
64
  def self.send_server_event
81
65
  Devcenter::Logger.log "Serving server side event to #{settings.connections.size} connections"
82
66
  settings.connections.each do |conn|
@@ -1,3 +1,3 @@
1
1
  module Devcenter
2
- VERSION = "1.0.7"
2
+ VERSION = "1.1.0rc1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devcenter
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.7
4
+ version: 1.1.0rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Raul Murciano
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-10-29 00:00:00.000000000 Z
12
+ date: 2013-12-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: listen
@@ -210,6 +210,7 @@ files:
210
210
  - bin/devcenter
211
211
  - devcenter.gemspec
212
212
  - lib/devcenter.rb
213
+ - lib/devcenter/article_file.rb
213
214
  - lib/devcenter/cli.rb
214
215
  - lib/devcenter/client.rb
215
216
  - lib/devcenter/coderay_extensions.rb
@@ -218,6 +219,7 @@ files:
218
219
  - lib/devcenter/commands/open.rb
219
220
  - lib/devcenter/commands/preview.rb
220
221
  - lib/devcenter/commands/pull.rb
222
+ - lib/devcenter/commands/push.rb
221
223
  - lib/devcenter/gem_version_checker.rb
222
224
  - lib/devcenter/helpers.rb
223
225
  - lib/devcenter/logger.rb
@@ -293,9 +295,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
293
295
  version: '0'
294
296
  required_rubygems_version: !ruby/object:Gem::Requirement
295
297
  requirements:
296
- - - ! '>='
298
+ - - ! '>'
297
299
  - !ruby/object:Gem::Version
298
- version: '0'
300
+ version: 1.3.1
299
301
  requirements: []
300
302
  rubyforge_project:
301
303
  rubygems_version: 2.0.3