├── .coffeelintignore ├── .gitignore ├── CONTRIBUTING.md ├── .travis.yml ├── menus └── sync-settings.cson ├── coffeelint.json ├── spec ├── spec-helpers.coffee └── sync-settings-spec.coffee ├── package.json ├── LICENSE.md ├── CHANGELOG.md ├── lib ├── config.coffee ├── tracker.coffee ├── package-manager.coffee └── sync-settings.coffee └── README.md /.coffeelintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please follow the guidelines defined by Atom in their [CONTRIBUTING.md](https://github.com/atom/atom/blob/master/CONTRIBUTING.md). 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | 3 | notifications: 4 | email: 5 | on_success: never 6 | on_failure: change 7 | 8 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh' 9 | -------------------------------------------------------------------------------- /menus/sync-settings.cson: -------------------------------------------------------------------------------- 1 | # See https://atom.io/docs/latest/creating-a-package#menus for more details 2 | 'menu': [ 3 | { 4 | 'label': 'Packages' 5 | 'submenu': [ 6 | 'label': 'Synchronize Settings' 7 | 'submenu': [ 8 | { 'label': 'Backup', 'command': 'sync-settings:backup' } 9 | { 'label': 'Restore', 'command': 'sync-settings:restore' } 10 | { 'label': 'View backup', 'command': 'sync-settings:view-backup' } 11 | { 'label': 'Check for updated backup', 'command': 'sync-settings:check-backup' } 12 | ] 13 | ] 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "max_line_length": { 3 | "level": "ignore" 4 | }, 5 | "no_empty_param_list": { 6 | "level": "error" 7 | }, 8 | "arrow_spacing": { 9 | "level": "error" 10 | }, 11 | "no_interpolation_in_single_quotes": { 12 | "level": "error" 13 | }, 14 | "no_debugger": { 15 | "level": "error" 16 | }, 17 | "prefer_english_operator": { 18 | "level": "error" 19 | }, 20 | "colon_assignment_spacing": { 21 | "spacing": { 22 | "left": 0, 23 | "right": 1 24 | }, 25 | "level": "error" 26 | }, 27 | "braces_spacing": { 28 | "spaces": 0, 29 | "level": "error" 30 | }, 31 | "spacing_after_comma": { 32 | "level": "error" 33 | }, 34 | "no_stand_alone_at": { 35 | "level": "error" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /spec/spec-helpers.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | setConfig: (keyPath, value) -> 3 | @originalConfigs ?= {} 4 | @originalConfigs[keyPath] ?= if atom.config.isDefault keyPath then null else atom.config.get keyPath 5 | atom.config.set keyPath, value 6 | 7 | restoreConfigs: -> 8 | if @originalConfigs 9 | for keyPath, value of @originalConfigs 10 | atom.config.set keyPath, value 11 | 12 | callAsync: (timeout, async, next) -> 13 | if typeof timeout is 'function' 14 | [async, next] = [timeout, async] 15 | timeout = 5000 16 | done = false 17 | nextArgs = null 18 | 19 | runs -> 20 | async (args...) -> 21 | done = true 22 | nextArgs = args 23 | 24 | 25 | waitsFor -> 26 | done 27 | , null, timeout 28 | 29 | if next? 30 | runs -> 31 | next.apply(this, nextArgs) 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sync-settings", 3 | "main": "./lib/sync-settings", 4 | "version": "0.6.0", 5 | "description": "Synchronize package settings, keymap and installed packages", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/Hackafe/atom-sync-settings" 9 | }, 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/Hackafe/atom-sync-settings/issues" 13 | }, 14 | "commits": { 15 | "url": "https://github.com/Hackafe/atom-sync-settings/commit" 16 | }, 17 | "engines": { 18 | "atom": ">=1.0.0 <2.0.0" 19 | }, 20 | "dependencies": { 21 | "analytics-node": "^1.2.2", 22 | "emissary": "1.x", 23 | "github": "0.*", 24 | "loophole": "^1.0.0", 25 | "node-uuid": "^1.4.1", 26 | "q": "~1.0.1", 27 | "semver": "~2.2.1", 28 | "underscore-plus": "^1.0.6" 29 | }, 30 | "devDependencies": { 31 | "coffeelint": "^1.10.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Geno Roupsky 2 | Copyright (c) 2014 Vassil Kalkov 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.6.0 4 | * Check for updated backup. Closes [#81](https://github.com/Hackafe/atom-sync-settings/issues/81) 5 | * New menu option to open the gist with external browser. Closes [#87](https://github.com/Hackafe/atom-sync-settings/issues/87) 6 | * Track usage. Closes [#82](https://github.com/Hackafe/atom-sync-settings/issues/82) 7 | 8 | ## v0.5.0 9 | * Fixed snippets not applied. Fixes [#36](https://github.com/Hackafe/atom-sync-settings/issues/36) 10 | * Please note that this issue created a redundant file called `snippets.coffee` 11 | * Rename Upload/Download to Backup/Restore. Fixes [#50](https://github.com/Hackafe/atom-sync-settings/issues/50) 12 | * Remove keymaps. Closes [#69](https://github.com/Hackafe/atom-sync-settings/issues/69) 13 | * Improve package load time. Fixes [#33](https://github.com/Hackafe/atom-sync-settings/issues/33) 14 | * Settings for which things to sync. Closes [#54](https://github.com/Hackafe/atom-sync-settings/issues/54) 15 | 16 | ## v0.4.0 17 | * Added default contents for empty files 18 | * Fix writing contents to extra files 19 | 20 | ## v0.3.0 21 | * Defer package activation until first upload/download 22 | * Added link to uploaded gist in success notification 23 | * Fixed deprecations 24 | * Update atom engine semver 25 | 26 | ## v0.2.2 27 | * Fixed deprecations 28 | * Fixed [#23](https://github.com/Hackafe/atom-sync-settings/issues/23) 29 | * Added extra files setting 30 | 31 | ## v0.2.1 32 | * Added notifications 33 | * Fixed deprecations 34 | 35 | ## v0.2.0 36 | * Sync user styles 37 | * Sync init 38 | * Sync snippets 39 | * Remove sensitive sync-settings setting data 40 | 41 | ## v0.1.0 42 | * First Release 43 | -------------------------------------------------------------------------------- /lib/config.coffee: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | personalAccessToken: 3 | description: 'Your personal GitHub access token' 4 | type: 'string' 5 | default: '' 6 | order: 1 7 | gistId: 8 | description: 'ID of gist to use for configuration storage' 9 | type: 'string' 10 | default: '' 11 | order: 2 12 | syncSettings: 13 | type: 'boolean' 14 | default: true 15 | order: 3 16 | syncPackages: 17 | type: 'boolean' 18 | default: true 19 | order: 4 20 | syncKeymap: 21 | type: 'boolean' 22 | default: true 23 | order: 5 24 | syncStyles: 25 | type: 'boolean' 26 | default: true 27 | order: 6 28 | syncInit: 29 | type: 'boolean' 30 | default: true 31 | order: 7 32 | syncSnippets: 33 | type: 'boolean' 34 | default: true 35 | order: 8 36 | extraFiles: 37 | description: 'Comma-seperated list of files other than Atom\'s default config files in ~/.atom' 38 | type: 'array' 39 | default: [] 40 | items: 41 | type: 'string' 42 | order: 9 43 | analytics: 44 | type: 'boolean' 45 | default: true 46 | description: "There is Segment.io which forwards data to Google 47 | Analytics to track what versions and platforms 48 | are used. Everything is anonymized and no personal information, such as source code, 49 | is sent. See the README.md for more details." 50 | order: 10 51 | _analyticsUserId: 52 | type: 'string' 53 | default: "" 54 | description: "Unique identifier for this user for tracking usage analytics" 55 | order: 11 56 | checkForUpdatedBackup: 57 | description: 'Check for newer backup on Atom start' 58 | type: 'boolean' 59 | default: true 60 | order: 12 61 | _lastBackupHash: 62 | type: 'string' 63 | default: '' 64 | description: 'Hash of the last backup restored or created' 65 | order: 13 66 | } 67 | -------------------------------------------------------------------------------- /lib/tracker.coffee: -------------------------------------------------------------------------------- 1 | # contants 2 | analyticsWriteKey = 'pDV1EgxAbco4gjPXpJzuOeDyYgtkrmmG' 3 | 4 | # imports 5 | _ = require 'underscore-plus' 6 | {allowUnsafeEval} = require 'loophole' 7 | 8 | # Analytics require a special import because of [Unsafe-Eval error](https://github.com/Glavin001/atom-beautify/commit/fbc58a648d3ccd845548d556f3dd1e046075bf04) 9 | Analytics = null 10 | allowUnsafeEval -> Analytics = require 'analytics-node' 11 | 12 | # load package.json to include package info in analytics 13 | pkg = require("../package.json") 14 | 15 | class Tracker 16 | 17 | constructor: (@analyticsUserIdConfigKey, @analyticsEnabledConfigKey) -> 18 | # Setup Analytics 19 | @analytics = new Analytics analyticsWriteKey 20 | 21 | # set a unique identifier 22 | if not atom.config.get @analyticsUserIdConfigKey 23 | uuid = require 'node-uuid' 24 | atom.config.set @analyticsUserIdConfigKey, uuid.v4() 25 | 26 | # default event properties 27 | @defaultEvent = 28 | userId: atom.config.get @analyticsUserIdConfigKey 29 | properties: 30 | value: 1 31 | version: atom.getVersion() 32 | platform: navigator.platform 33 | category: "Atom-#{atom.getVersion()}/#{pkg.name}-#{pkg.version}" 34 | context: 35 | app: 36 | name: pkg.name 37 | version: pkg.version 38 | userAgent: navigator.userAgent 39 | 40 | # identify the user 41 | atom.config.observe @analyticsUserIdConfigKey, (userId) => 42 | @analytics.identify 43 | userId: userId 44 | @defaultEvent.userId = userId 45 | 46 | # cache enabled and watch for changes 47 | @enabled = atom.config.get @analyticsEnabledConfigKey 48 | atom.config.onDidChange @analyticsEnabledConfigKey, ({newValue}) => 49 | @enabled = newValue 50 | 51 | track: (message) -> 52 | return if not @enabled 53 | message = event: message if _.isString(message) 54 | console.debug "tracking #{message.event}" 55 | @analytics.track _.deepExtend(@defaultEvent, message) 56 | 57 | trackActivate: -> 58 | @track 59 | event: 'Activate' 60 | properties: 61 | label: pkg.version 62 | 63 | trackDeactivate: -> 64 | @track 65 | event: 'Deactivate' 66 | properties: 67 | label: pkg.version 68 | 69 | error: (e) -> 70 | @track 71 | event: 'Error' 72 | properties: 73 | error: e 74 | 75 | module.exports = Tracker 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sync Settings for Atom 2 | 3 | [![Join the chat at https://gitter.im/Hackafe/atom-sync-settings](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Hackafe/atom-sync-settings?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Build Status](https://travis-ci.org/Hackafe/atom-sync-settings.svg?branch=master)](https://travis-ci.org/Hackafe/atom-sync-settings) 5 | 6 | Synchronize settings, keymaps, user styles, init script, snippets and installed packages across [Atom](http://atom.io) instances. 7 | 8 | ## Features 9 | * Sync Atom's and package settings 10 | * Sync installed packages 11 | * Sync user keymaps 12 | * Sync user styles 13 | * Sync user init script 14 | * Sync snippets 15 | * Sync user defined text files 16 | * Manual backup/restore to a gist 17 | 18 | ## Installation 19 | 20 | `$ apm install sync-settings` or using the Install packages pane from [Atom Settings](atom://config). 21 | 22 | ## Setup 23 | 24 | 1. Open **Sync Settings** configuration in [Atom Settings](atom://config). 25 | 2. Create a [new personal access token](https://github.com/settings/tokens/new) which has the `gist` scope. 26 | 3. Copy the access token to **Sync Settings** configuration. 27 | 4. Create a [new gist](https://gist.github.com/) and save it. 28 | 5. Copy the gist id (last part of url after the username) to **Sync Settings** configuration. 29 | 30 | ## Usage 31 | 32 | Backup or restore all settings from the Packages menu or use one of the following **commands**: 33 | * `sync-settings:backup` 34 | * `sync-settings:restore` 35 | 36 | View your online backup using the following command: 37 | * `sync-settings:view-backup` 38 | 39 | Check the latest backup is applied: 40 | * `sync-settings:check-backup` 41 | 42 | ## Contributing 43 | 44 | If you're going to submit a pull request, please try to follow 45 | [the official contribution guidelines of Atom](https://atom.io/docs/latest/contributing). 46 | 47 | 1. [Fork it](https://github.com/Hackafe/atom-sync-settings/). 48 | 2. Create your feature branch (`git checkout -b my-new-feature`). 49 | 3. Commit your changes (`git commit -am 'Add some feature'`). 50 | 4. Push to the branch (`git push origin my-new-feature`). 51 | 5. Create new Pull Request. 52 | 53 | [See all contributors](https://github.com/Hackafe/atom-sync-settings/graphs/contributors). 54 | 55 | ## Privacy 56 | 57 | There is [Segment.io](https://segment.io/) which forwards data to [Google Analytics](http://www.google.com/analytics/) to track what versions and platforms 58 | are used. Everything is anonymized and no personal information, such as source code, 59 | is sent. See https://github.com/Hackafe/atom-sync-settings/issues/82 for more details. 60 | It can be disabled from package settings. 61 | -------------------------------------------------------------------------------- /lib/package-manager.coffee: -------------------------------------------------------------------------------- 1 | ## copied from https://github.com/atom/settings-view 2 | 3 | 4 | _ = require 'underscore-plus' 5 | {BufferedProcess} = require 'atom' 6 | {Emitter} = require 'emissary' 7 | Q = require 'q' 8 | semver = require 'semver' 9 | url = require 'url' 10 | 11 | Q.stopUnhandledRejectionTracking() 12 | 13 | module.exports = 14 | class PackageManager 15 | Emitter.includeInto(this) 16 | 17 | constructor: -> 18 | @packagePromises = [] 19 | 20 | runCommand: (args, callback) -> 21 | command = atom.packages.getApmPath() 22 | outputLines = [] 23 | stdout = (lines) -> outputLines.push(lines) 24 | errorLines = [] 25 | stderr = (lines) -> errorLines.push(lines) 26 | exit = (code) -> 27 | callback(code, outputLines.join('\n'), errorLines.join('\n')) 28 | 29 | args.push('--no-color') 30 | new BufferedProcess({command, args, stdout, stderr, exit}) 31 | 32 | loadFeatured: (callback) -> 33 | args = ['featured', '--json'] 34 | version = atom.getVersion() 35 | args.push('--compatible', version) if semver.valid(version) 36 | 37 | @runCommand args, (code, stdout, stderr) -> 38 | if code is 0 39 | try 40 | packages = JSON.parse(stdout) ? [] 41 | catch error 42 | callback(error) 43 | return 44 | 45 | callback(null, packages) 46 | else 47 | error = new Error('Fetching featured packages and themes failed.') 48 | error.stdout = stdout 49 | error.stderr = stderr 50 | callback(error) 51 | 52 | loadOutdated: (callback) -> 53 | args = ['outdated', '--json'] 54 | version = atom.getVersion() 55 | args.push('--compatible', version) if semver.valid(version) 56 | 57 | @runCommand args, (code, stdout, stderr) -> 58 | if code is 0 59 | try 60 | packages = JSON.parse(stdout) ? [] 61 | catch error 62 | callback(error) 63 | return 64 | 65 | callback(null, packages) 66 | else 67 | error = new Error('Fetching outdated packages and themes failed.') 68 | error.stdout = stdout 69 | error.stderr = stderr 70 | callback(error) 71 | 72 | loadPackage: (packageName, callback) -> 73 | args = ['view', packageName, '--json'] 74 | 75 | @runCommand args, (code, stdout, stderr) -> 76 | if code is 0 77 | try 78 | packages = JSON.parse(stdout) ? [] 79 | catch error 80 | callback(error) 81 | return 82 | 83 | callback(null, packages) 84 | else 85 | error = new Error("Fetching package '#{packageName}' failed.") 86 | error.stdout = stdout 87 | error.stderr = stderr 88 | callback(error) 89 | 90 | getFeatured: -> 91 | @featuredPromise ?= Q.nbind(@loadFeatured, this)() 92 | 93 | getOutdated: -> 94 | @outdatedPromise ?= Q.nbind(@loadOutdated, this)() 95 | 96 | getPackage: (packageName) -> 97 | @packagePromises[packageName] ?= Q.nbind(@loadPackage, this, packageName)() 98 | 99 | search: (query, options = {}) -> 100 | deferred = Q.defer() 101 | 102 | args = ['search', query, '--json'] 103 | if options.themes 104 | args.push '--themes' 105 | else if options.packages 106 | args.push '--packages' 107 | 108 | @runCommand args, (code, stdout, stderr) -> 109 | if code is 0 110 | try 111 | packages = JSON.parse(stdout) ? [] 112 | deferred.resolve(packages) 113 | catch error 114 | deferred.reject(error) 115 | else 116 | error = new Error("Searching for \u201C#{query}\u201D failed.") 117 | error.stdout = stdout 118 | error.stderr = stderr 119 | deferred.reject(error) 120 | 121 | deferred.promise 122 | 123 | update: (pack, newVersion, callback) -> 124 | {name, theme} = pack 125 | 126 | activateOnSuccess = not theme and not atom.packages.isPackageDisabled(name) 127 | activateOnFailure = atom.packages.isPackageActive(name) 128 | atom.packages.deactivatePackage(name) if atom.packages.isPackageActive(name) 129 | atom.packages.unloadPackage(name) if atom.packages.isPackageLoaded(name) 130 | 131 | args = ['install', "#{name}@#{newVersion}"] 132 | exit = (code, stdout, stderr) => 133 | if code is 0 134 | if activateOnSuccess 135 | atom.packages.activatePackage(name) 136 | else 137 | atom.packages.loadPackage(name) 138 | 139 | callback?() 140 | @emitPackageEvent 'updated', pack 141 | else 142 | atom.packages.activatePackage(name) if activateOnFailure 143 | error = new Error("Updating to \u201C#{name}@#{newVersion}\u201D failed.") 144 | error.stdout = stdout 145 | error.stderr = stderr 146 | error.packageInstallError = not theme 147 | @emitPackageEvent 'update-failed', pack, error 148 | callback(error) 149 | 150 | @emit('package-updating', pack) 151 | @runCommand(args, exit) 152 | 153 | install: (pack, callback) -> 154 | {name, version, theme} = pack 155 | activateOnSuccess = not theme and not atom.packages.isPackageDisabled(name) 156 | activateOnFailure = atom.packages.isPackageActive(name) 157 | atom.packages.deactivatePackage(name) if atom.packages.isPackageActive(name) 158 | atom.packages.unloadPackage(name) if atom.packages.isPackageLoaded(name) 159 | 160 | args = ['install', "#{name}@#{version}"] 161 | exit = (code, stdout, stderr) => 162 | if code is 0 163 | if activateOnSuccess 164 | atom.packages.activatePackage(name) 165 | else 166 | atom.packages.loadPackage(name) 167 | 168 | callback?() 169 | @emitPackageEvent 'installed', pack 170 | else 171 | atom.packages.activatePackage(name) if activateOnFailure 172 | error = new Error("Installing \u201C#{name}@#{version}\u201D failed.") 173 | error.stdout = stdout 174 | error.stderr = stderr 175 | error.packageInstallError = not theme 176 | @emitPackageEvent 'install-failed', pack, error 177 | callback(error) 178 | 179 | @runCommand(args, exit) 180 | 181 | uninstall: (pack, callback) -> 182 | {name} = pack 183 | 184 | atom.packages.deactivatePackage(name) if atom.packages.isPackageActive(name) 185 | 186 | @runCommand ['uninstall', '--hard', name], (code, stdout, stderr) => 187 | if code is 0 188 | atom.packages.unloadPackage(name) if atom.packages.isPackageLoaded(name) 189 | callback?() 190 | @emitPackageEvent 'uninstalled', pack 191 | else 192 | error = new Error("Uninstalling \u201C#{name}\u201D failed.") 193 | error.stdout = stdout 194 | error.stderr = stderr 195 | @emitPackageEvent 'uninstall-failed', pack, error 196 | callback(error) 197 | 198 | canUpgrade: (installedPackage, availableVersion) -> 199 | return false unless installedPackage? 200 | 201 | installedVersion = installedPackage.metadata.version 202 | return false unless semver.valid(installedVersion) 203 | return false unless semver.valid(availableVersion) 204 | 205 | semver.gt(availableVersion, installedVersion) 206 | 207 | getPackageTitle: ({name}) -> 208 | _.undasherize(_.uncamelcase(name)) 209 | 210 | getRepositoryUrl: ({metadata}) -> 211 | {repository} = metadata 212 | repoUrl = repository?.url ? repository ? '' 213 | repoUrl.replace(/\.git$/, '').replace(/\/+$/, '') 214 | 215 | getAuthorUserName: (pack) -> 216 | return null unless repoUrl = @getRepositoryUrl(pack) 217 | repoName = url.parse(repoUrl).pathname 218 | chunks = repoName.match '/(.+?)/' 219 | chunks?[1] 220 | 221 | checkNativeBuildTools: -> 222 | deferred = Q.defer() 223 | 224 | @runCommand ['install', '--check'], (code, stdout, stderr) -> 225 | if code is 0 226 | deferred.resolve() 227 | else 228 | deferred.reject(new Error()) 229 | 230 | deferred.promise 231 | 232 | # Emits the appropriate event for the given package. 233 | # 234 | # All events are either of the form `theme-foo` or `package-foo` depending on 235 | # whether the event is for a theme or a normal package. This method standardizes 236 | # the logic to determine if a package is a theme or not and formats the event 237 | # name appropriately. 238 | # 239 | # eventName - The event name suffix {String} of the event to emit. 240 | # pack - The package for which the event is being emitted. 241 | # error - Any error information to be included in the case of an error. 242 | emitPackageEvent: (eventName, pack, error) -> 243 | theme = pack.theme ? pack.metadata?.theme 244 | eventName = if theme then "theme-#{eventName}" else "package-#{eventName}" 245 | @emit eventName, pack, error 246 | -------------------------------------------------------------------------------- /lib/sync-settings.coffee: -------------------------------------------------------------------------------- 1 | # imports 2 | {BufferedProcess} = require 'atom' 3 | fs = require 'fs' 4 | _ = require 'underscore-plus' 5 | [GitHubApi, PackageManager, Tracker] = [] 6 | 7 | # constants 8 | DESCRIPTION = 'Atom configuration storage operated by http://atom.io/packages/sync-settings' 9 | REMOVE_KEYS = ["sync-settings"] 10 | 11 | SyncSettings = 12 | config: require('./config.coffee') 13 | 14 | activate: -> 15 | # speedup activation by async initializing 16 | setImmediate => 17 | # actual initialization after atom has loaded 18 | GitHubApi ?= require 'github' 19 | PackageManager ?= require './package-manager' 20 | Tracker ?= require './tracker' 21 | 22 | atom.commands.add 'atom-workspace', "sync-settings:backup", => 23 | @backup() 24 | @tracker.track 'Backup' 25 | atom.commands.add 'atom-workspace', "sync-settings:restore", => 26 | @restore() 27 | @tracker.track 'Restore' 28 | atom.commands.add 'atom-workspace', "sync-settings:view-backup", => 29 | @viewBackup() 30 | @tracker.track 'View backup' 31 | atom.commands.add 'atom-workspace', "sync-settings:check-backup", => 32 | @checkForUpdate() 33 | @tracker.track 'Check backup' 34 | 35 | @checkForUpdate() if atom.config.get('sync-settings.checkForUpdatedBackup') 36 | 37 | # make the tracking last in case any exception happens 38 | @tracker = new Tracker 'sync-settings._analyticsUserId', 'sync-settings.analytics' 39 | @tracker.trackActivate() 40 | 41 | deactivate: -> 42 | @tracker.trackDeactivate() 43 | 44 | serialize: -> 45 | 46 | checkForUpdate: (cb=null) -> 47 | if atom.config.get('sync-settings.gistId') 48 | console.debug('checking latest backup...') 49 | @createClient().gists.get 50 | id: atom.config.get 'sync-settings.gistId' 51 | , (err, res) => 52 | console.debug(err, res) 53 | if err 54 | console.error "error while retrieving the gist. does it exists?", err 55 | try 56 | message = JSON.parse(err.message).message 57 | message = 'Gist ID Not Found' if message is 'Not Found' 58 | catch SyntaxError 59 | message = err.message 60 | atom.notifications.addError "sync-settings: Error retrieving your settings. ("+message+")" 61 | return cb?() 62 | 63 | console.debug("latest backup version #{res.history[0].version}") 64 | if res.history[0].version isnt atom.config.get('sync-settings._lastBackupHash') 65 | @notifyNewerBackup() 66 | else 67 | @notifyBackupUptodate() 68 | 69 | cb?() 70 | else 71 | @notifyMissingGistId() 72 | 73 | 74 | 75 | notifyNewerBackup: -> 76 | # we need the actual element for dispatching on it 77 | workspaceElement = atom.views.getView(atom.workspace) 78 | notification = atom.notifications.addWarning "sync-settings: Your settings are out of date.", 79 | dismissable: true 80 | buttons: [{ 81 | text: "Backup" 82 | onDidClick: -> 83 | atom.commands.dispatch workspaceElement, "sync-settings:backup" 84 | notification.dismiss() 85 | }, { 86 | text: "View backup" 87 | onDidClick: -> 88 | atom.commands.dispatch workspaceElement, "sync-settings:view-backup" 89 | }, { 90 | text: "Restore" 91 | onDidClick: -> 92 | atom.commands.dispatch workspaceElement, "sync-settings:restore" 93 | notification.dismiss() 94 | }, { 95 | text: "Dismiss" 96 | onDidClick: -> notification.dismiss() 97 | }] 98 | 99 | notifyBackupUptodate: -> 100 | atom.notifications.addSuccess "sync-settings: Latest backup is already applied." 101 | 102 | notifyMissingGistId: -> 103 | atom.notifications.addError "sync-settings: Missing gist ID" 104 | 105 | backup: (cb=null) -> 106 | files = {} 107 | if atom.config.get('sync-settings.syncSettings') 108 | files["settings.json"] = content: JSON.stringify(atom.config.settings, @filterSettings, '\t') 109 | if atom.config.get('sync-settings.syncPackages') 110 | files["packages.json"] = content: JSON.stringify(@getPackages(), null, '\t') 111 | if atom.config.get('sync-settings.syncKeymap') 112 | files["keymap.cson"] = content: (@fileContent atom.keymaps.getUserKeymapPath()) ? "# keymap file (not found)" 113 | if atom.config.get('sync-settings.syncStyles') 114 | files["styles.less"] = content: (@fileContent atom.styles.getUserStyleSheetPath()) ? "// styles file (not found)" 115 | if atom.config.get('sync-settings.syncInit') 116 | files["init.coffee"] = content: (@fileContent atom.config.configDirPath + "/init.coffee") ? "# initialization file (not found)" 117 | if atom.config.get('sync-settings.syncSnippets') 118 | files["snippets.cson"] = content: (@fileContent atom.config.configDirPath + "/snippets.cson") ? "# snippets file (not found)" 119 | 120 | for file in atom.config.get('sync-settings.extraFiles') ? [] 121 | ext = file.slice(file.lastIndexOf(".")).toLowerCase() 122 | cmtstart = "#" 123 | cmtstart = "//" if ext in [".less", ".scss", ".js"] 124 | cmtstart = "/*" if ext in [".css"] 125 | cmtend = "" 126 | cmtend = "*/" if ext in [".css"] 127 | files[file] = 128 | content: (@fileContent atom.config.configDirPath + "/#{file}") ? "#{cmtstart} #{file} (not found) #{cmtend}" 129 | 130 | @createClient().gists.edit 131 | id: atom.config.get 'sync-settings.gistId' 132 | description: "automatic update by http://atom.io/packages/sync-settings" 133 | files: files 134 | , (err, res) -> 135 | if err 136 | console.error "error backing up data: "+err.message, err 137 | message = JSON.parse(err.message).message 138 | message = 'Gist ID Not Found' if message is 'Not Found' 139 | atom.notifications.addError "sync-settings: Error backing up your settings. ("+message+")" 140 | else 141 | atom.config.set('sync-settings._lastBackupHash', res.history[0].version) 142 | atom.notifications.addSuccess "sync-settings: Your settings were successfully backed up.
Click here to open your Gist." 143 | cb?(err, res) 144 | 145 | viewBackup: -> 146 | Shell = require 'shell' 147 | gistId = atom.config.get 'sync-settings.gistId' 148 | Shell.openExternal "https://gist.github.com/#{gistId}" 149 | 150 | getPackages: -> 151 | for own name, info of atom.packages.getLoadedPackages() 152 | {name, version, theme} = info.metadata 153 | {name, version, theme} 154 | 155 | restore: (cb=null) -> 156 | @createClient().gists.get 157 | id: atom.config.get 'sync-settings.gistId' 158 | , (err, res) => 159 | if err 160 | console.error "error while retrieving the gist. does it exists?", err 161 | message = JSON.parse(err.message).message 162 | message = 'Gist ID Not Found' if message is 'Not Found' 163 | atom.notifications.addError "sync-settings: Error retrieving your settings. ("+message+")" 164 | return 165 | 166 | callbackAsync = false 167 | 168 | for own filename, file of res.files 169 | switch filename 170 | when 'settings.json' 171 | @applySettings '', JSON.parse(file.content) if atom.config.get('sync-settings.syncSettings') 172 | 173 | when 'packages.json' 174 | if atom.config.get('sync-settings.syncPackages') 175 | callbackAsync = true 176 | @installMissingPackages JSON.parse(file.content), cb 177 | 178 | when 'keymap.cson' 179 | fs.writeFileSync atom.keymaps.getUserKeymapPath(), file.content if atom.config.get('sync-settings.syncKeymap') 180 | 181 | when 'styles.less' 182 | fs.writeFileSync atom.styles.getUserStyleSheetPath(), file.content if atom.config.get('sync-settings.syncStyles') 183 | 184 | when 'init.coffee' 185 | fs.writeFileSync atom.config.configDirPath + "/init.coffee", file.content if atom.config.get('sync-settings.syncInit') 186 | 187 | when 'snippets.cson' 188 | fs.writeFileSync atom.config.configDirPath + "/snippets.cson", file.content if atom.config.get('sync-settings.syncSnippets') 189 | 190 | else fs.writeFileSync "#{atom.config.configDirPath}/#{filename}", file.content 191 | 192 | atom.config.set('sync-settings._lastBackupHash', res.history[0].version) 193 | 194 | atom.notifications.addSuccess "sync-settings: Your settings were successfully synchronized." 195 | 196 | cb() unless callbackAsync 197 | 198 | createClient: -> 199 | token = atom.config.get 'sync-settings.personalAccessToken' 200 | console.debug "Creating GitHubApi client with token = #{token}" 201 | github = new GitHubApi 202 | version: '3.0.0' 203 | # debug: true 204 | protocol: 'https' 205 | github.authenticate 206 | type: 'oauth' 207 | token: token 208 | github 209 | 210 | filterSettings: (key, value) -> 211 | return value if key is "" 212 | return undefined if ~REMOVE_KEYS.indexOf(key) 213 | value 214 | 215 | applySettings: (pref, settings) -> 216 | for key, value of settings 217 | keyPath = "#{pref}.#{key}" 218 | if _.isObject(value) and not _.isArray(value) 219 | @applySettings keyPath, value 220 | else 221 | console.debug "config.set #{keyPath[1...]}=#{value}" 222 | atom.config.set keyPath[1...], value 223 | 224 | installMissingPackages: (packages, cb) -> 225 | pending=0 226 | for pkg in packages 227 | continue if atom.packages.isPackageLoaded(pkg.name) 228 | pending++ 229 | @installPackage pkg, -> 230 | pending-- 231 | cb?() if pending is 0 232 | cb?() if pending is 0 233 | 234 | installPackage: (pack, cb) -> 235 | type = if pack.theme then 'theme' else 'package' 236 | console.info("Installing #{type} #{pack.name}...") 237 | packageManager = new PackageManager() 238 | packageManager.install pack, (error) -> 239 | if error? 240 | console.error("Installing #{type} #{pack.name} failed", error.stack ? error, error.stderr) 241 | else 242 | console.info("Installed #{type} #{pack.name}") 243 | cb?(error) 244 | 245 | fileContent: (filePath) -> 246 | try 247 | return fs.readFileSync(filePath, {encoding: 'utf8'}) or null 248 | catch e 249 | console.error "Error reading file #{filePath}. Probably doesn't exist.", e 250 | null 251 | 252 | module.exports = SyncSettings 253 | -------------------------------------------------------------------------------- /spec/sync-settings-spec.coffee: -------------------------------------------------------------------------------- 1 | SyncSettings = require '../lib/sync-settings' 2 | SpecHelper = require './spec-helpers' 3 | run = SpecHelper.callAsync 4 | fs = require 'fs' 5 | # Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs. 6 | # 7 | # To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit` 8 | # or `fdescribe`). Remove the `f` to unfocus the block. 9 | 10 | describe "SyncSettings", -> 11 | 12 | describe "low-level", -> 13 | describe "::fileContent", -> 14 | it "returns null for not existing file", -> 15 | expect(SyncSettings.fileContent("/tmp/atom-sync-settings.tmp")).toBeNull() 16 | 17 | it "returns null for empty file", -> 18 | fs.writeFileSync "/tmp/atom-sync-settings.tmp", "" 19 | try 20 | expect(SyncSettings.fileContent("/tmp/atom-sync-settings.tmp")).toBeNull() 21 | finally 22 | fs.unlinkSync "/tmp/atom-sync-settings.tmp" 23 | 24 | it "returns content of existing file", -> 25 | text = "alabala portocala" 26 | fs.writeFileSync "/tmp/atom-sync-settings.tmp", text 27 | try 28 | expect(SyncSettings.fileContent("/tmp/atom-sync-settings.tmp")).toEqual text 29 | finally 30 | fs.unlinkSync "/tmp/atom-sync-settings.tmp" 31 | 32 | describe "high-level", -> 33 | TOKEN_CONFIG = 'sync-settings.personalAccessToken' 34 | GIST_ID_CONFIG = 'sync-settings.gistId' 35 | 36 | window.resetTimeouts() 37 | SyncSettings.activate() 38 | window.advanceClock() 39 | 40 | beforeEach -> 41 | @token = process.env.GITHUB_TOKEN or atom.config.get(TOKEN_CONFIG) 42 | atom.config.set(TOKEN_CONFIG, @token) 43 | 44 | run (cb) -> 45 | gistSettings = 46 | public: false 47 | description: "Test gist by Sync Settings for Atom https://github.com/Hackafe/atom-sync-settings" 48 | files: README: content: '# Generated by Sync Settings for Atom https://github.com/Hackafe/atom-sync-settings' 49 | SyncSettings.createClient().gists.create(gistSettings, cb) 50 | , (err, res) => 51 | expect(err).toBeNull() 52 | 53 | @gistId = res.id 54 | console.log "Using Gist #{@gistId}" 55 | atom.config.set(GIST_ID_CONFIG, @gistId) 56 | 57 | afterEach -> 58 | run (cb) => 59 | SyncSettings.createClient().gists.delete {id: @gistId}, cb 60 | , (err, res) -> 61 | expect(err).toBeNull() 62 | 63 | describe "::backup", -> 64 | it "back up the settings", -> 65 | atom.config.set('sync-settings.syncSettings', true) 66 | run (cb) -> 67 | SyncSettings.backup cb 68 | , -> 69 | run (cb) => 70 | SyncSettings.createClient().gists.get({id: @gistId}, cb) 71 | , (err, res) -> 72 | expect(res.files['settings.json']).toBeDefined() 73 | 74 | it "don't back up the settings", -> 75 | atom.config.set('sync-settings.syncSettings', false) 76 | run (cb) -> 77 | SyncSettings.backup cb 78 | , -> 79 | run (cb) => 80 | SyncSettings.createClient().gists.get({id: @gistId}, cb) 81 | , (err, res) -> 82 | expect(res.files['settings.json']).not.toBeDefined() 83 | 84 | it "back up the installed packages list", -> 85 | atom.config.set('sync-settings.syncPackages', true) 86 | run (cb) -> 87 | SyncSettings.backup cb 88 | , -> 89 | run (cb) => 90 | SyncSettings.createClient().gists.get({id: @gistId}, cb) 91 | , (err, res) -> 92 | expect(res.files['packages.json']).toBeDefined() 93 | 94 | it "don't back up the installed packages list", -> 95 | atom.config.set('sync-settings.syncPackages', false) 96 | run (cb) -> 97 | SyncSettings.backup cb 98 | , -> 99 | run (cb) => 100 | SyncSettings.createClient().gists.get({id: @gistId}, cb) 101 | , (err, res) -> 102 | expect(res.files['packages.json']).not.toBeDefined() 103 | 104 | it "back up the user keymaps", -> 105 | atom.config.set('sync-settings.syncKeymap', true) 106 | run (cb) -> 107 | SyncSettings.backup cb 108 | , -> 109 | run (cb) => 110 | SyncSettings.createClient().gists.get({id: @gistId}, cb) 111 | , (err, res) -> 112 | expect(res.files['keymap.cson']).toBeDefined() 113 | 114 | it "don't back up the user keymaps", -> 115 | atom.config.set('sync-settings.syncKeymap', false) 116 | run (cb) -> 117 | SyncSettings.backup cb 118 | , -> 119 | run (cb) => 120 | SyncSettings.createClient().gists.get({id: @gistId}, cb) 121 | , (err, res) -> 122 | expect(res.files['keymap.cson']).not.toBeDefined() 123 | 124 | it "back up the user styles", -> 125 | atom.config.set('sync-settings.syncStyles', true) 126 | run (cb) -> 127 | SyncSettings.backup cb 128 | , -> 129 | run (cb) => 130 | SyncSettings.createClient().gists.get({id: @gistId}, cb) 131 | , (err, res) -> 132 | expect(res.files['styles.less']).toBeDefined() 133 | 134 | it "don't back up the user styles", -> 135 | atom.config.set('sync-settings.syncStyles', false) 136 | run (cb) -> 137 | SyncSettings.backup cb 138 | , -> 139 | run (cb) => 140 | SyncSettings.createClient().gists.get({id: @gistId}, cb) 141 | , (err, res) -> 142 | expect(res.files['styles.less']).not.toBeDefined() 143 | 144 | it "back up the user init.coffee file", -> 145 | atom.config.set('sync-settings.syncInit', true) 146 | run (cb) -> 147 | SyncSettings.backup cb 148 | , -> 149 | run (cb) => 150 | SyncSettings.createClient().gists.get({id: @gistId}, cb) 151 | , (err, res) -> 152 | expect(res.files['init.coffee']).toBeDefined() 153 | 154 | it "don't back up the user init.coffee file", -> 155 | atom.config.set('sync-settings.syncInit', false) 156 | run (cb) -> 157 | SyncSettings.backup cb 158 | , -> 159 | run (cb) => 160 | SyncSettings.createClient().gists.get({id: @gistId}, cb) 161 | , (err, res) -> 162 | expect(res.files['init.coffee']).not.toBeDefined() 163 | 164 | it "back up the user snippets", -> 165 | atom.config.set('sync-settings.syncSnippets', true) 166 | run (cb) -> 167 | SyncSettings.backup cb 168 | , -> 169 | run (cb) => 170 | SyncSettings.createClient().gists.get({id: @gistId}, cb) 171 | , (err, res) -> 172 | expect(res.files['snippets.cson']).toBeDefined() 173 | 174 | it "don't back up the user snippets", -> 175 | atom.config.set('sync-settings.syncSnippets', false) 176 | run (cb) -> 177 | SyncSettings.backup cb 178 | , -> 179 | run (cb) => 180 | SyncSettings.createClient().gists.get({id: @gistId}, cb) 181 | , (err, res) -> 182 | expect(res.files['snippets.cson']).not.toBeDefined() 183 | 184 | it "back up the files defined in config.extraFiles", -> 185 | atom.config.set 'sync-settings.extraFiles', ['test.tmp', 'test2.tmp'] 186 | run (cb) -> 187 | SyncSettings.backup cb 188 | , -> 189 | run (cb) => 190 | SyncSettings.createClient().gists.get({id: @gistId}, cb) 191 | , (err, res) -> 192 | for file in atom.config.get 'sync-settings.extraFiles' 193 | expect(res.files[file]).toBeDefined() 194 | 195 | it "don't back up extra files defined in config.extraFiles", -> 196 | atom.config.set 'sync-settings.extraFiles', undefined 197 | run (cb) -> 198 | SyncSettings.backup cb 199 | , -> 200 | run (cb) => 201 | SyncSettings.createClient().gists.get({id: @gistId}, cb) 202 | , (err, res) -> 203 | expect(Object.keys(res.files).length).toBe(1) 204 | 205 | describe "::restore", -> 206 | it "updates settings", -> 207 | atom.config.set('sync-settings.syncSettings', true) 208 | atom.config.set "some-dummy", true 209 | run (cb) -> 210 | SyncSettings.backup cb 211 | , -> 212 | atom.config.set "some-dummy", false 213 | run (cb) -> 214 | SyncSettings.restore cb 215 | , -> 216 | expect(atom.config.get "some-dummy").toBeTruthy() 217 | 218 | it "doesn't updates settings", -> 219 | atom.config.set('sync-settings.syncSettings', false) 220 | atom.config.set "some-dummy", true 221 | run (cb) -> 222 | SyncSettings.backup cb 223 | , -> 224 | run (cb) -> 225 | SyncSettings.restore cb 226 | , -> 227 | expect(atom.config.get "some-dummy").toBeTruthy() 228 | 229 | it "overrides keymap.cson", -> 230 | atom.config.set('sync-settings.syncKeymap', true) 231 | original = SyncSettings.fileContent(atom.keymaps.getUserKeymapPath()) ? "# keymap file (not found)" 232 | run (cb) -> 233 | SyncSettings.backup cb 234 | , -> 235 | fs.writeFileSync atom.keymaps.getUserKeymapPath(), "#{original}\n# modified by sync setting spec" 236 | run (cb) -> 237 | SyncSettings.restore cb 238 | , -> 239 | expect(SyncSettings.fileContent(atom.keymaps.getUserKeymapPath())).toEqual original 240 | fs.writeFileSync atom.keymaps.getUserKeymapPath(), original 241 | 242 | it "restores all other files in the gist as well", -> 243 | atom.config.set 'sync-settings.extraFiles', ['test.tmp', 'test2.tmp'] 244 | run (cb) -> 245 | SyncSettings.backup cb 246 | , -> 247 | run (cb) -> 248 | SyncSettings.restore cb 249 | , -> 250 | for file in atom.config.get 'sync-settings.extraFiles' 251 | expect(fs.existsSync("#{atom.config.configDirPath}/#{file}")).toBe(true) 252 | expect(SyncSettings.fileContent("#{atom.config.configDirPath}/#{file}")).toBe("# #{file} (not found) ") 253 | fs.unlink "#{atom.config.configDirPath}/#{file}" 254 | 255 | describe "::check for update", -> 256 | 257 | beforeEach -> 258 | atom.config.unset 'sync-settings._lastBackupHash' 259 | 260 | it "updates last hash on backup", -> 261 | run (cb) -> 262 | SyncSettings.backup cb 263 | , -> 264 | expect(atom.config.get "sync-settings._lastBackupHash").toBeDefined() 265 | 266 | it "updates last hash on restore", -> 267 | run (cb) -> 268 | SyncSettings.restore cb 269 | , -> 270 | expect(atom.config.get "sync-settings._lastBackupHash").toBeDefined() 271 | 272 | describe "::notification", -> 273 | beforeEach -> 274 | atom.notifications.clear() 275 | 276 | it "displays on newer backup", -> 277 | run (cb) -> 278 | SyncSettings.checkForUpdate cb 279 | , -> 280 | expect(atom.notifications.getNotifications().length).toBe(1) 281 | expect(atom.notifications.getNotifications()[0].getType()).toBe('warning') 282 | 283 | it "ignores on up-to-date backup", -> 284 | run (cb) -> 285 | SyncSettings.backup cb 286 | , -> 287 | run (cb) -> 288 | atom.notifications.clear() 289 | SyncSettings.checkForUpdate cb 290 | , -> 291 | expect(atom.notifications.getNotifications().length).toBe(1) 292 | expect(atom.notifications.getNotifications()[0].getType()).toBe('success') 293 | --------------------------------------------------------------------------------