├── .rspec ├── spec ├── fixtures │ ├── New-Plugin-1.0 │ │ ├── .gitignore │ │ ├── New plugin.sketchplugin │ │ ├── README.md │ │ └── chest.json │ ├── Sketch-StickyGrid-1.0 │ │ ├── .gitignore │ │ ├── chest.json │ │ ├── README.md │ │ └── Snap to Grid.sketchplugin │ └── Sketch-StickyGrid-0.1 │ │ ├── chest.json │ │ └── Snap to Grid.sketchplugin ├── chest_spec.rb ├── config_spec.rb ├── spec_helper.rb ├── plugin_folder_spec.rb ├── cli_spec.rb └── registry_spec.rb ├── Gemfile ├── lib ├── chest │ ├── version.rb │ ├── helper.rb │ ├── registry.rb │ ├── config.rb │ ├── plugin_folder.rb │ └── cli.rb └── chest.rb ├── .travis.yml ├── .gitignore ├── assets └── readme_images │ └── logo.png ├── tool ├── console └── setup ├── Rakefile ├── bin └── chest ├── .editorconfig ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── chest.gemspec ├── CODE_OF_CONDUCT.md ├── .rubocop.yml └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /spec/fixtures/New-Plugin-1.0/.gitignore: -------------------------------------------------------------------------------- 1 | assets 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /spec/fixtures/Sketch-StickyGrid-1.0/.gitignore: -------------------------------------------------------------------------------- 1 | something 2 | -------------------------------------------------------------------------------- /spec/fixtures/New-Plugin-1.0/New plugin.sketchplugin: -------------------------------------------------------------------------------- 1 | // New plugin 2 | -------------------------------------------------------------------------------- /lib/chest/version.rb: -------------------------------------------------------------------------------- 1 | module Chest 2 | VERSION = '2.0.0'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | - 2.3.1 5 | sudo: false 6 | -------------------------------------------------------------------------------- /spec/fixtures/New-Plugin-1.0/README.md: -------------------------------------------------------------------------------- 1 | # New Plugin 2 | 3 | this is new plugin. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /vendor/bundle/ 3 | /Gemfile.lock 4 | /pkg/ 5 | /spec/reports/ 6 | /tmp/ 7 | -------------------------------------------------------------------------------- /assets/readme_images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sketch-Chest/chest/HEAD/assets/readme_images/logo.png -------------------------------------------------------------------------------- /spec/fixtures/Sketch-StickyGrid-0.1/chest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "StickyGrid", 3 | "version": "0.1.0" 4 | } 5 | -------------------------------------------------------------------------------- /lib/chest/helper.rb: -------------------------------------------------------------------------------- 1 | module Chest 2 | def sanitize_name(name) 3 | name.gsub(/[\s\/]/, '-') 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /tool/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'chest' 5 | 6 | require 'pry' 7 | Pry.start 8 | -------------------------------------------------------------------------------- /tool/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install --path vendor/bundle --binstubs .bundle/bin -j4 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /spec/chest_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chest do 4 | it 'has a version number' do 5 | expect(Chest::VERSION).not_to be nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /bin/chest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'chest' 4 | 5 | begin 6 | Chest::CLI.start ARGV 7 | rescue SystemExit, Interrupt => err 8 | puts err 9 | rescue StandardError => err 10 | puts err 11 | end 12 | -------------------------------------------------------------------------------- /lib/chest.rb: -------------------------------------------------------------------------------- 1 | require 'chest/version' 2 | require 'chest/helper' 3 | require 'chest/plugin_folder' 4 | require 'chest/config' 5 | require 'chest/registry' 6 | require 'chest/cli' 7 | 8 | # Chest module 9 | module Chest 10 | end 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [Unreleased] - Unreleased 2 | 3 | - Integrate Chest registry service(URL: TBD) 4 | 5 | # [2.0.0] - 2016-05-08 6 | 7 | - Add #init, #info, #open command 8 | - Support for new plugin architecture since Sketch 3.3 9 | 10 | # [1.0.0] - 2015-03-02 11 | 12 | - Support for reducted format of plugin specification 13 | 14 | # [0.1.0] - 2015-02-15 15 | 16 | - First release 17 | -------------------------------------------------------------------------------- /spec/fixtures/New-Plugin-1.0/chest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "New-Plugin", 3 | "version": "1.0.0", 4 | "description": "All new plugin", 5 | "keywords": [], 6 | "repository": "https://github.com/uetchy/New-Plugin.git", 7 | "authors": [ 8 | "Yasuaki Uechi " 9 | ], 10 | "license": "ISC", 11 | "homepage": "https://github.com/uetchy/New-Plugin" 12 | } 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | ## Build 4 | 5 | ``` 6 | $ git clone https://github.com/Sketch-Chest/chest.git 7 | $ cd chest 8 | $ bundle install 9 | $ bundle exec rake build 10 | ``` 11 | 12 | ## Test 13 | 14 | > Build this project before testing. 15 | 16 | ``` 17 | $ bundle exec rspec 18 | ``` 19 | 20 | ## Submit pull request 21 | 22 | Go to [Pull Requests](https://github.com/Sketch-Chest/chest/pulls). 23 | -------------------------------------------------------------------------------- /spec/fixtures/Sketch-StickyGrid-1.0/chest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "StickyGrid", 3 | "version": "1.0.0", 4 | "description": "Sketch plugin to make paths be snapped to grid.", 5 | "keywords": ["grid"], 6 | "repository": "https://github.com/uetchy/Sketch-Chest.git", 7 | "authors": [ 8 | "Yasuaki Uechi " 9 | ], 10 | "license": "MIT", 11 | "homepage": "https://github.com/uetchy/Sketch-Chest" 12 | } 13 | -------------------------------------------------------------------------------- /spec/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chest::Config do 4 | it 'can create instance' do 5 | Dir.mktmpdir do |tmpdir| 6 | path = File.join tmpdir, '.chestrc' 7 | 8 | config = Chest::Config.new(path) 9 | expect(config.to_hash).to eq({}) 10 | config.test = 1 11 | expect(config.to_hash).to eq(test: 1) 12 | config.save 13 | expect(File.open(path).read.strip).to eq(config.to_json) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'webmock/rspec' 3 | require 'pry-byebug' 4 | 5 | require 'chest' 6 | 7 | RSpec.configure do |_config| 8 | def capture(stream) 9 | begin 10 | stream = stream.to_s 11 | eval "$#{stream} = StringIO.new" 12 | yield 13 | result = eval("$#{stream}").string 14 | ensure 15 | eval("$#{stream} = #{stream.upcase}") 16 | end 17 | 18 | result 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/fixtures/Sketch-StickyGrid-1.0/README.md: -------------------------------------------------------------------------------- 1 | # StickyGrid 2 | 3 | Sketch plugin to make paths be snapped to grid. 4 | 5 | ![](https://raw.githubusercontent.com/uetchy/Sketch-StickyGrid/master/assets/readme_images/stickygrid.gif) 6 | 7 | ## Installation 8 | 9 | 1. [Download the plugin](https://github.com/uetchy/Sketch-StickyGrid/archive/master.zip) 10 | 2. Unzip the archive 11 | 3. Place the folder into your Sketch Plugins folder. 12 | 13 | ## Usage 14 | 15 | ### Snap to Grid `ctrl` + `⌘` + `G` 16 | -------------------------------------------------------------------------------- /spec/plugin_folder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chest::PluginFolder do 4 | before :each do 5 | @endpoint = 'http://sketchchest.com/api/' 6 | @token = 'dummytoken' 7 | @plugin = { 8 | name: 'StickyGrid', 9 | version: '1.0.0', 10 | license: 'MIT License', 11 | git_url: 'https://github.com/uetchy/Sketch-StickyGrid.git' 12 | } 13 | end 14 | 15 | it 'can install package' 16 | # do 17 | # plugin_names = [ 18 | # 'StickyGrid', # index 19 | # 'uetchy/Sketch-StickyGrid', # github 20 | # 'https://github.com/uetchy/Sketch-StickyGrid.git' #git 21 | # ] 22 | # plugin_names.each do |plugin_name| 23 | # Dir.mktmpdir do |tmpdir| 24 | # @registry.install_package(plugin_name, tmpdir) 25 | # expect(File.exist?(File.join(tmpdir, '*.sketchplugin/Contents/Sketch/manifest.json'))).to eq(true) 26 | # # expect(JSON.parse(File.open(File.join(tmpdir, 'chest.json')).read)['version']).to eq('1.0.0') 27 | # end 28 | # end 29 | # end 30 | end 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yasuaki Uechi (https://randompaper.co) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fileutils' 3 | 4 | describe Chest::CLI do 5 | before :each do 6 | @plugin_repo_name = 'Sketch-StickyGrid' 7 | @plugin_name = 'StickyGrid' 8 | @plugin_version = '1.0.0' 9 | @plugin_folder_path = Chest::PluginFolder::SKETCH_PLUGIN_FOLDER_PATH 10 | FileUtils.mkdir_p(@plugin_folder_path) 11 | 12 | @expected_plugin_path = File.join(@plugin_folder_path, @plugin_repo_name) 13 | end 14 | 15 | context 'install' do 16 | let(:output) { capture(:stdout) { subject.install('uetchy/Sketch-StickyGrid') } } 17 | 18 | it 'install plugin' do 19 | expect(output).to include('installed') 20 | end 21 | 22 | it 'find out plugin in Sketch plugin folder' do 23 | expect(Dir.exist?(@expected_plugin_path)).to be true 24 | end 25 | end 26 | 27 | context 'info' do 28 | let(:output) { capture(:stdout) { subject.info(@plugin_name) } } 29 | 30 | it 'return plugin info' do 31 | expect(output).to include(@plugin_name) 32 | end 33 | end 34 | 35 | context 'list' do 36 | let(:output) { capture(:stdout) { subject.list } } 37 | 38 | it 'returns a list' do 39 | expect(output).to include(@plugin_name) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /chest.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | lib = File.expand_path('../lib', __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'chest/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'chest' 9 | spec.version = Chest::VERSION 10 | spec.authors = ['Yasuaki Uechi'] 11 | spec.email = ['uetchy@randompaper.co'] 12 | 13 | spec.summary = 'The lightweight plugin manager for Sketch.app' 14 | spec.homepage = 'https://github.com/Sketch-Chest/chest' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = 'bin' 19 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 20 | spec.require_paths = ['lib'] 21 | 22 | spec.add_dependency 'thor', '~> 0.19.1' 23 | spec.add_dependency 'rest-client', '~> 1.8.0' 24 | spec.add_dependency 'semantic', '~> 1.4.1' 25 | spec.add_dependency 'parseconfig', '~> 1.0.6' 26 | spec.add_dependency 'git', '~> 1.2.9' 27 | 28 | spec.add_development_dependency 'bundler' 29 | spec.add_development_dependency 'rake' 30 | spec.add_development_dependency 'rspec' 31 | spec.add_development_dependency 'pry-byebug' 32 | spec.add_development_dependency 'webmock' 33 | end 34 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at 14 | -------------------------------------------------------------------------------- /lib/chest/registry.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'rest_client' 3 | require 'uri' 4 | require 'fileutils' 5 | 6 | class Chest::Registry 7 | def initialize(token = nil, endpoint: 'http://sketchchest.com/api/') 8 | @token = token 9 | @endpoint = endpoint 10 | end 11 | 12 | def request_raw(method, path, params = {}) 13 | case method 14 | when :get 15 | RestClient.get path, params: params 16 | when :post 17 | RestClient.post(path, params, content_type: :json, accept: :json) 18 | when :delete 19 | RestClient.delete(path, params: params) 20 | end 21 | end 22 | 23 | def request(method, path, params = {}) 24 | params[:token] = @token 25 | response = request_raw(method, URI.join(@endpoint, path).to_s, params) 26 | JSON.parse(response.body) 27 | end 28 | 29 | def fetch_package(package_name) 30 | request :get, "packages/#{package_name}.json" 31 | end 32 | 33 | def normalize_to_git_url(query) 34 | if query =~ /\.git$/ 35 | return query 36 | elsif query =~ /\A([a-zA-Z0-9_\-]+)\/([a-zA-Z0-9_\-]+)\z/ 37 | user = Regexp.last_match(1) 38 | repository = Regexp.last_match(2) 39 | url = "https://github.com/#{user}/#{repository}.git" 40 | return url 41 | else 42 | package = fetch_package(query) 43 | if package['error'] 44 | raise InvalidArgumentError, "Specify valid query for #{query}" 45 | end 46 | return package['git_url'] 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/chest/config.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'ostruct' 3 | require 'fileutils' 4 | 5 | module Chest 6 | CONFIG_BASE_DIR = File.expand_path('.config/chest', '~') 7 | CONFIG_PATH = File.join(CONFIG_BASE_DIR, 'config.json') 8 | 9 | class FileMissingError < StandardError; end 10 | 11 | class Config < OpenStruct 12 | attr_reader :file_path 13 | 14 | def initialize(file_path = CONFIG_PATH) 15 | super({}) 16 | @file_path = file_path 17 | load! 18 | end 19 | 20 | def load! 21 | if File.exist? @file_path 22 | File.open(@file_path, 'r') do |f| 23 | marshal_load(symbolize_keys(JSON(f.read))) 24 | end 25 | end 26 | rescue Errno::ENOENT, IOError 27 | raise FileMissingError, @file_path 28 | end 29 | 30 | def method_missing(name, *args) 31 | super(name, *args) 32 | end 33 | 34 | def update!(attributes = {}) 35 | attributes_with!(attributes) 36 | end 37 | 38 | def attributes_with!(attributes = {}) 39 | attributes.each do |key, value| 40 | send(key.to_s + '=', value) if respond_to?(key.to_s + '=') 41 | end 42 | end 43 | 44 | def save 45 | FileUtils.mkpath(File.dirname(@file_path)) 46 | File.open(@file_path, 'w') { |f| f.puts to_json } 47 | end 48 | 49 | def to_hash 50 | table.to_h 51 | end 52 | 53 | def to_json 54 | JSON.pretty_generate(to_hash) 55 | end 56 | 57 | private 58 | 59 | def symbolize_keys(hash) 60 | hash.each_with_object({}) do |(k, v), res| 61 | res[k.to_sym] = v 62 | res 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Lint/EndAlignment: 2 | Exclude: 3 | - 'lib/chest/cli.rb' 4 | 5 | # Offense count: 3 6 | Lint/Eval: 7 | Exclude: 8 | - 'spec/spec_helper.rb' 9 | 10 | # Offense count: 4 11 | Metrics/AbcSize: 12 | Max: 31 13 | 14 | # Offense count: 1 15 | # Configuration parameters: CountComments. 16 | Metrics/ClassLength: 17 | Max: 115 18 | 19 | # Offense count: 16 20 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes. 21 | # URISchemes: http, https 22 | Metrics/LineLength: 23 | Max: 184 24 | 25 | # Offense count: 6 26 | # Configuration parameters: CountComments. 27 | Metrics/MethodLength: 28 | Max: 25 29 | 30 | # Offense count: 2 31 | # Configuration parameters: EnforcedStyle, SupportedStyles. 32 | # SupportedStyles: nested, compact 33 | Style/ClassAndModuleChildren: 34 | Exclude: 35 | - 'lib/chest/cli.rb' 36 | - 'lib/chest/registry.rb' 37 | 38 | # Offense count: 5 39 | Style/Documentation: 40 | Exclude: 41 | - 'spec/**/*' 42 | - 'test/**/*' 43 | - 'lib/chest/cli.rb' 44 | - 'lib/chest/config.rb' 45 | - 'lib/chest/helper.rb' 46 | - 'lib/chest/plugin_folder.rb' 47 | - 'lib/chest/registry.rb' 48 | 49 | # Offense count: 2 50 | # Configuration parameters: MinBodyLength. 51 | Style/GuardClause: 52 | Exclude: 53 | - 'lib/chest/plugin_folder.rb' 54 | - 'lib/chest/registry.rb' 55 | 56 | # Offense count: 1 57 | Style/MethodMissing: 58 | Exclude: 59 | - 'lib/chest/config.rb' 60 | 61 | # Offense count: 2 62 | # Cop supports --auto-correct. 63 | # Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes. 64 | # SupportedStyles: slashes, percent_r, mixed 65 | Style/RegexpLiteral: 66 | Exclude: 67 | - 'lib/chest/helper.rb' 68 | - 'lib/chest/registry.rb' 69 | -------------------------------------------------------------------------------- /spec/fixtures/Sketch-StickyGrid-0.1/Snap to Grid.sketchplugin: -------------------------------------------------------------------------------- 1 | // Snap to Grid (ctrl cmd g) 2 | 3 | (function() { 4 | 5 | var app = [NSApplication sharedApplication]; 6 | var grid_interval = doc.grid().gridSize(); 7 | 8 | var compute_position = function(pos, intval) { 9 | return Math.round(pos / intval) * intval; 10 | }; 11 | 12 | var adjust_points = function(shape_layer) { 13 | var path = shape_layer.path(); 14 | var points_count = path.numberOfPoints(); 15 | var artboard_frame = shape_layer.frameInArtboard() 16 | 17 | for (var j=0; j < points_count; j++) { 18 | var point = path.pointAtIndex(j); 19 | 20 | var abs_point = shape_layer.absolutePoint(point.point()); 21 | 22 | var rel_x = compute_position( 23 | artboard_frame.x() + abs_point.x, 24 | grid_interval) - (artboard_frame.x() + abs_point.x); 25 | 26 | var rel_y = compute_position( 27 | artboard_frame.y() + abs_point.y, 28 | grid_interval) - (artboard_frame.y() + abs_point.y); 29 | 30 | var cg_point = shape_layer.relativePoint( 31 | CGPointMake( 32 | abs_point.x + rel_x, 33 | abs_point.y + rel_y 34 | )); 35 | 36 | point.movePointTo(cg_point); 37 | } 38 | 39 | shape_layer.adjustFrameAfterEdit(); 40 | } 41 | 42 | for (var i=0; i < [selection count]; i++) { 43 | var object = [selection objectAtIndex: i]; 44 | 45 | if ([object isKindOfClass:[MSShapePathLayer class]]) { 46 | // MSShapeGroup 47 | adjust_points(object); 48 | } else if ([object isMemberOfClass:[MSLayerGroup class]]) { 49 | // MSLayerGroup 50 | for (var l=0; l < [[object layers] count]; l++) { 51 | var shape_layer_group = [[object layers] objectAtIndex:l]; 52 | 53 | for (var l2=0; l2 < [[shape_layer_group layers] count]; l2++) 54 | adjust_points([[shape_layer_group layers] objectAtIndex:l2]); 55 | } 56 | } else { 57 | // MSShapePathLayer 58 | for (var l=0; l < [[object layers] count]; l++) 59 | adjust_points([[object layers] objectAtIndex:l]); 60 | } 61 | } 62 | 63 | })(); 64 | -------------------------------------------------------------------------------- /spec/fixtures/Sketch-StickyGrid-1.0/Snap to Grid.sketchplugin: -------------------------------------------------------------------------------- 1 | // Snap to Grid (ctrl cmd g) 2 | 3 | (function() { 4 | 5 | var app = [NSApplication sharedApplication]; 6 | var grid_interval = doc.grid().gridSize(); 7 | 8 | var compute_position = function(pos, intval) { 9 | return Math.round(pos / intval) * intval; 10 | }; 11 | 12 | var adjust_points = function(shape_layer) { 13 | var path = shape_layer.path(); 14 | var points_count = path.numberOfPoints(); 15 | var artboard_frame = shape_layer.frameInArtboard() 16 | 17 | for (var j=0; j < points_count; j++) { 18 | var point = path.pointAtIndex(j); 19 | 20 | var abs_point = shape_layer.absolutePoint(point.point()); 21 | 22 | var rel_x = compute_position( 23 | artboard_frame.x() + abs_point.x, 24 | grid_interval) - (artboard_frame.x() + abs_point.x); 25 | 26 | var rel_y = compute_position( 27 | artboard_frame.y() + abs_point.y, 28 | grid_interval) - (artboard_frame.y() + abs_point.y); 29 | 30 | var cg_point = shape_layer.relativePoint( 31 | CGPointMake( 32 | abs_point.x + rel_x, 33 | abs_point.y + rel_y 34 | )); 35 | 36 | point.movePointTo(cg_point); 37 | } 38 | 39 | shape_layer.adjustFrameAfterEdit(); 40 | } 41 | 42 | for (var i=0; i < [selection count]; i++) { 43 | var object = [selection objectAtIndex: i]; 44 | 45 | if ([object isKindOfClass:[MSShapePathLayer class]]) { 46 | // MSShapeGroup 47 | adjust_points(object); 48 | } else if ([object isMemberOfClass:[MSLayerGroup class]]) { 49 | // MSLayerGroup 50 | for (var l=0; l < [[object layers] count]; l++) { 51 | var shape_layer_group = [[object layers] objectAtIndex:l]; 52 | 53 | for (var l2=0; l2 < [[shape_layer_group layers] count]; l2++) 54 | adjust_points([[shape_layer_group layers] objectAtIndex:l2]); 55 | } 56 | } else { 57 | // MSShapePathLayer 58 | for (var l=0; l < [[object layers] count]; l++) 59 | adjust_points([[object layers] objectAtIndex:l]); 60 | } 61 | } 62 | 63 | })(); 64 | -------------------------------------------------------------------------------- /lib/chest/plugin_folder.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module Chest 4 | class PluginFolder 5 | SKETCH_PLUGIN_FOLDER_PATH = File.expand_path('~/Library/Application Support/com.bohemiancoding.sketch3/Plugins/').freeze 6 | SKETCH_APPSTORE_PLUGIN_FOLDER_PATH = File.expand_path('~/Library/Containers/com.bohemiancoding.sketch3/Data/Library/Application Support/com.bohemiancoding.sketch3/Plugins/').freeze 7 | 8 | class InvalidArgumentError < StandardError; end 9 | 10 | def initialize 11 | @registry = Chest::Registry.new 12 | end 13 | 14 | def manifest_for(plugin_path) 15 | manifest_path = Dir.glob(File.join(plugin_path, '*.sketchplugin/Contents/Sketch/manifest.json')).first 16 | JSON.parse(File.open(manifest_path).read) 17 | end 18 | 19 | def path_for(name, include_manifest = false) 20 | exact_plugin_path = File.join(SKETCH_PLUGIN_FOLDER_PATH, name) 21 | return exact_plugin_path unless include_manifest 22 | plugins.each do |plugin_path| 23 | if manifest_for(plugin_path)['name'] == name || File.identical?(plugin_path, exact_plugin_path) 24 | return plugin_path 25 | end 26 | end 27 | 28 | nil 29 | end 30 | 31 | def plugins 32 | Dir.glob(File.join(SKETCH_PLUGIN_FOLDER_PATH, '*/')) 33 | end 34 | 35 | def install(source_path, plugin_name) 36 | destination_path = path_for(plugin_name) 37 | raise "#{plugin_name} already installed" if Dir.exist? destination_path 38 | FileUtils.cp_r(source_path, destination_path) 39 | end 40 | 41 | def uninstall(plugin_path) 42 | if Dir.exist? plugin_path 43 | FileUtils.rm_rf(plugin_path) 44 | return plugin_path 45 | else 46 | raise "#{plugin_path} doesn't exist" 47 | end 48 | end 49 | 50 | def update 51 | fetch_method = "update_#{type}" 52 | if respond_to?(fetch_method, true) 53 | begin 54 | send(fetch_method) 55 | rescue => e 56 | raise "#{@name}: #{e}" 57 | else 58 | manifest = Manifest.new 59 | manifest.add_plugin(@name, to_option) 60 | manifest.save 61 | end 62 | else 63 | raise "Unknown strategy type: #{type}" 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://raw.githubusercontent.com/Sketch-Chest/chest/master/assets/readme_images/logo.png) 2 | 3 | [![Build Status](https://travis-ci.org/Sketch-Chest/chest.svg?branch=master)](https://travis-ci.org/Sketch-Chest/chest) [![Join the chat at https://gitter.im/Sketch-Chest/chest](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Sketch-Chest/chest?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | The lightweight plugin manager for Sketch. 6 | 7 | This software requires OS X Mavericks or later. 8 | 9 | ## Install 10 | 11 | ```console 12 | $ gem install chest 13 | ``` 14 | 15 | ## Usage 16 | 17 | You can install Sketch plugins which is hosted on GitHub by using `install` command: 18 | 19 | ```console 20 | $ chest install uetchy/Sketch-StickyGrid 21 | ``` 22 | 23 | Or just specify Git url: 24 | 25 | ```console 26 | $ chest install https://github.com/uetchy/Sketch-StickyGrid.git 27 | ``` 28 | 29 | Also you can use `uninstall`, `update`, `list`, et al.: 30 | 31 | ```console 32 | $ chest uninstall StickyGrid # delete from Plugins folder 33 | $ chest update StickyGrid # pull from git 34 | $ chest update # update all plugins 35 | $ chest list # list installed plugins 36 | $ chest info # show plugin information 37 | $ chest init # generate manifest.json for your plugin 38 | $ chest open # open plugin folder in Finder 39 | ``` 40 | 41 | To see all of available commands, use `help` command: 42 | 43 | ```console 44 | $ chest help 45 | ``` 46 | 47 | ## Development 48 | 49 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment. Run `bundle exec chest` to use the code located in this directory, ignoring other installed copies of this gem. 50 | 51 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 52 | 53 | ## Contributing 54 | 55 | Issues are welcome! 56 | 57 | 1. Fork it ( ) 58 | 2. Create your feature branch (`git checkout -b my-new-feature`) 59 | 3. Commit your changes (`git commit -am 'Add some feature'`) 60 | 4. Push to the branch (`git push origin my-new-feature`) 61 | 5. Create a new Pull Request 62 | -------------------------------------------------------------------------------- /spec/registry_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Chest::Registry do 4 | before :all do 5 | @endpoint = 'http://sketchchest.com/api' 6 | @token = 'dummytoken' 7 | @plugin = { 8 | name: 'StickyGrid', 9 | version: '1.0.0', 10 | license: 'MIT License' 11 | } 12 | 13 | stub_request(:get, URI.join(@endpoint, "packages/#{@plugin[:name]}.json")) 14 | .with(query: { token: @token }) 15 | .to_return(body: @plugin.to_json) 16 | 17 | stub_request(:post, URI.join(@endpoint, '/packages.json')) 18 | .with(query: { token: @token }) 19 | .to_return(body: @plugin.to_json) 20 | 21 | @registry = Chest::Registry.new(@token, endpoint: @endpoint) 22 | end 23 | 24 | it 'can fetch plugin information' do 25 | result = @registry.fetch_package(@plugin[:name]) 26 | 27 | expect(result).to be_a_kind_of Hash 28 | expect(result['name']).to eq @plugin[:name] 29 | expect(result['version']).to eq @plugin[:version] 30 | expect(result['license']).to eq @plugin[:license] 31 | end 32 | 33 | it 'can publish package' do 34 | old_version = @registry.publish_package('spec/fixtures/Sketch-StickyGrid-0.1') 35 | expect(old_version).not_to have_key 'error' 36 | new_version = @registry.publish_package('spec/fixtures/Sketch-StickyGrid-1.0') 37 | expect(new_version).not_to have_key 'error' 38 | 39 | new_plugin = @registry.publish_package('spec/fixtures/New-Plugin-1.0') 40 | expect(new_plugin).not_to have_key 'error' 41 | end 42 | # 43 | # it 'cannot publish old package' do 44 | # status = @registry.publish_package('spec/fixtures/Sketch-StickyGrid-0.1') 45 | # pp status 46 | # expect(status).to have_key 'error' 47 | # end 48 | # 49 | # it 'cannot publish a package that have same version of latest package' do 50 | # status = @registry.publish_package('spec/fixtures/Sketch-StickyGrid-1.0') 51 | # pp status 52 | # expect(status).to have_key 'error' 53 | # end 54 | 55 | # it 'can fetch versions index' do 56 | # name = 'StickyGrid' 57 | # info = @registry.fetch_package_versions(name) 58 | # 59 | # expect(info).to be_a_kind_of Hash 60 | # expect(info).to have_key 'versions' 61 | # expect(info['versions']).to be_a_kind_of Array 62 | # expect(info['versions'].size).to eq 2 63 | # end 64 | # 65 | # it 'can download package' do 66 | # name = 'StickyGrid' 67 | # Dir.mktmpdir do |tmpdir| 68 | # @registry.download_package(name, 'latest', tmpdir) 69 | # expect(File.exist?(File.join(tmpdir, 'chest.json'))).to eq(true) 70 | # expect(JSON.parse(File.open(File.join(tmpdir, 'chest.json')).read)['version']).to eq('1.0.0') 71 | # end 72 | # end 73 | # 74 | # it 'can download old package' do 75 | # name = 'StickyGrid' 76 | # Dir.mktmpdir do |tmpdir| 77 | # @registry.download_package(name, '0.1.0', tmpdir) 78 | # expect(File.exist?(File.join(tmpdir, 'chest.json'))).to eq(true) 79 | # expect(JSON.parse(File.open(File.join(tmpdir, 'chest.json')).read)['version']).to eq('0.1.0') 80 | # end 81 | # end 82 | # 83 | # it 'will ignore files specified in .gitignore' do 84 | # name = 'New-Plugin' 85 | # Dir.mktmpdir do |tmpdir| 86 | # @registry.download_package(name, 'latest', tmpdir) 87 | # expect(Dir.exist?(File.join(tmpdir, 'assets'))).not_to eq true 88 | # end 89 | # end 90 | # 91 | # it 'can unpublish plugin' do 92 | # status = @registry.unpublish_package('StickyGrid') 93 | # expect(status).to have_key 'status' 94 | # status = @registry.unpublish_package('New-Plugin') 95 | # expect(status).to have_key 'status' 96 | # end 97 | end 98 | -------------------------------------------------------------------------------- /lib/chest/cli.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | require 'fileutils' 3 | require 'json' 4 | require 'git' 5 | 6 | class Chest::CLI < Thor 7 | def initialize(*args) 8 | super 9 | end 10 | 11 | desc 'version', "Prints the bundler's version information" 12 | def version 13 | say "Chest version #{Chest::VERSION}" 14 | end 15 | map %w(-v --version) => :version 16 | 17 | desc 'install NAME', 'Install plugin' 18 | def install(query) 19 | git_url = Chest::Registry.new.normalize_to_git_url(query) 20 | plugin_folder = Chest::PluginFolder.new 21 | 22 | begin 23 | say "===> Cloning #{git_url}" 24 | Dir.mktmpdir do |tmpdir| 25 | repo = Git.clone(git_url, 'p', path: tmpdir) 26 | remote_path = URI.parse(repo.remote.url).path 27 | repo_name = File.basename(remote_path, File.extname(remote_path)) 28 | plugin_folder.install(File.join(tmpdir, 'p'), repo_name) 29 | info(repo_name) 30 | end 31 | rescue => e 32 | say '===> Error', :red 33 | raise e 34 | else 35 | say '💎 Successfully installed' 36 | end 37 | end 38 | 39 | desc 'uninstall NAME', 'Uninstall plugin' 40 | def uninstall(plugin_name) 41 | plugin_folder = Chest::PluginFolder.new 42 | 43 | begin 44 | plugin_path = plugin_folder.path_for(plugin_name, true) 45 | 46 | raise "#{plugin_name} doesn't exist" unless Dir.exist? plugin_path 47 | 48 | delete = yes? "Are you sure to uninstall '#{plugin_name}'? (y/n)" 49 | if delete 50 | say '===> Uninstalling' 51 | deleted_path = plugin_folder.uninstall(plugin_path) 52 | say "Deleted: #{deleted_path}" 53 | end 54 | rescue => e 55 | say '===> Error', :red 56 | raise e.to_s 57 | end 58 | end 59 | 60 | desc 'update [NAME]', 'Update plugins' 61 | def update(plugin_name = nil) 62 | plugin_folder = Chest::PluginFolder.new 63 | plugins = plugin_name ? [plugin_folder.path_for(plugin_name, true)] : plugin_folder.plugins 64 | 65 | say '===> Updating plugins' 66 | plugins.each do |plugin_path| 67 | begin 68 | manifest = plugin_folder.manifest_for(plugin_path) 69 | repo = Git.open(plugin_path) 70 | repo.pull 71 | rescue => e 72 | say "Error: #{e}", :red 73 | else 74 | new_manifest = plugin_folder.manifest_for(plugin_path) 75 | say "Updated #{manifest['name']} (#{manifest['version']} > #{new_manifest['version']})", :green 76 | end 77 | end 78 | end 79 | 80 | desc 'info NAME', 'Show plugin info' 81 | def info(plugin_name) 82 | plugin_folder = Chest::PluginFolder.new 83 | plugin_path = plugin_folder.path_for(plugin_name, true) 84 | raise "#{plugin_name} doesn't exist" unless Dir.exist? plugin_path 85 | 86 | manifest = plugin_folder.manifest_for(plugin_path) 87 | say "#{manifest['name']}: #{manifest['version']}" 88 | say manifest['description'].to_s 89 | say "Author: #{manifest['author']}" 90 | say manifest['homepage'].to_s 91 | end 92 | 93 | desc 'list', 'List plugins' 94 | def list 95 | plugin_folder = Chest::PluginFolder.new 96 | plugins = plugin_folder.plugins 97 | plugins.each do |plugin_path| 98 | manifest = plugin_folder.manifest_for(plugin_path) 99 | say "#{manifest['name']} (#{manifest['version']})" 100 | end 101 | end 102 | 103 | desc 'init', 'Create manifest.json' 104 | def init 105 | package = {} 106 | 107 | say 'Creating manifest.json ...' 108 | 109 | # Name 110 | package['name'] = ask 'name:', default: File.basename(Dir.pwd) 111 | 112 | # Version 113 | package['version'] = ask 'version:', default: '1.0.0' 114 | 115 | # Description 116 | package['description'] = ask 'description:' 117 | 118 | # Keywords 119 | package['keywords'] = [ask('keywords:')] 120 | 121 | # Authors 122 | git_user = `git config --get user.name`.strip 123 | git_email = `git config --get user.email`.strip 124 | package['author'] = ask('author:', default: git_user) 125 | package['authorEmail'] = ask('authorEmail:', default: git_email) 126 | 127 | # License 128 | package['license'] = ask 'license:', default: 'MIT' 129 | 130 | # Homepage 131 | remote_url = `git config --get remote.origin.url`.strip 132 | package['homepage'] = ask 'homepage:', if remote_url =~ %r{github\.com[:\/]([a-zA-Z0-9_-]+?)\/([a-zA-Z0-9_\-]+?)\.git} 133 | { default: "https://github.com/#{Regexp.last_match(1)}/#{Regexp.last_match(2)}" } 134 | end 135 | 136 | # Repository 137 | package['repository'] = remote_url 138 | 139 | say "\n" 140 | json = JSON.pretty_generate(package) 141 | say json 142 | if yes? 'Looks good?', :green 143 | if File.exist?('manifest.json') && !file_collision('manifest.json') 144 | raise SystemExit 145 | end 146 | File.open('manifest.json', 'w').write(json) 147 | end 148 | end 149 | 150 | desc 'open', 'Open plugins folder' 151 | def open 152 | system %(/usr/bin/open "#{Chest::PluginFolder::SKETCH_PLUGIN_FOLDER_PATH}") 153 | end 154 | end 155 | --------------------------------------------------------------------------------