disable chat corruption  automatic fixing, since it leads to a feedback loop of death

--- src/smc-webapp/editor_chat.cjsx
+++ src/smc-webapp/editor_chat.cjsx
@@ -260,9 +260,15 @@
                 if not syncdb.valid_data
                     # This should never happen, but obviously it can -- just open the file and randomly edit with vim!
                     # If there were any corrupted chats, append them as a new chat at the bottom, then delete from syncdb.
+                    ###
+                    # DISABLING THIS -- it leads to feedback loops of death.
                     corrupted = (x.corrupt for x in syncdb.select() when x.corrupt?)
                     actions.send_chat("Corrupted chat: " + corrupted.join('\n\n'))
                     syncdb.delete_with_field(field:'corrupt')
+                    ###
+                    console.warn("'#{path}' contains some illegable chats -- you may need to use TimeTravel")
+                    # See https://github.com/sagemathinc/smc/issues/944 for our plan regarding making corruption impossible.
+                    # Basically, we will switch to a restricted patch format.

                 actions.init_from_syncdb()
                 syncdb.on('change', actions._syncdb_change)


webapp/utm: only notes, fixing setup

webapp/utm: only notes, fixing setup

--- src/smc-webapp/misc_page.coffee
+++ src/smc-webapp/misc_page.coffee
@@ -1854,10 +1854,35 @@

 # If there is UTM information in the known cookie, extract and return it
 # Then, delete this cookie.
+# Reference: https://en.wikipedia.org/wiki/UTM_parameters
+#
+# Parameter                 Purpose/Example
+# utm_source (required)     Identifies which site sent the traffic, and is a required parameter.
+#                           utm_source=Google
+#
+# utm_medium                Identifies what type of link was used,
+#                           such as cost per click or email.
+#                           utm_medium=cpc
+# utm_campaign              Identifies a specific product promotion or strategic campaign.
+#                           utm_campaign=spring_sale
+# utm_term                  Identifies search terms.
+#                           utm_term=running+shoes
+# utm_content               Identifies what specifically was clicked to bring the user to the site,
+#                           such as a banner ad or a text link. It is often used for A/B testing
+#                           and content-targeted ads.
+#                           utm_content=logolink or utm_content=textlink
+#
+# the name of the cookie where we temporarily store this information as json
 utm_cookie_name = 'CC_UTM'
 # returns: either undefined or a dict of utm params and their values
 exports.get_utm = ->
     c = exports.get_cookie(utm_cookie_name)
     if DEBUG then console.log("UTM", c)
     exports.delete_cookie(utm_cookie_name)
-    return undefined
+    return misc.from_json(c)
+
+# store eventually available information form the url parameters in the utm cookie
+# do not overwrite any available information
+exports.save_utm_info = ->
+    c = exports.get_cookie(utm_cookie_name)
+


add support for super(...) in MethodDefinitions.

--- lib/closure-conversion.coffee
+++ lib/closure-conversion.coffee
@@ -34,8 +34,8 @@
 #
 # to:
 #
-# let Subclass = (function (_super) {
-#   %extends(Subclass, _super);
+# let Subclass = (function(%super) {
+#   %extends(Subclass, %super);
 #   function Subclass (/* ctor args */) { /* ctor body */ }
 #   Subclass.prototype.method = function(/* method args */) { /* method body */ };
 #   return Subclass;
@@ -45,8 +45,45 @@
 DesugarClasses = class DesugarClasses extends TreeVisitor
         constructor: ->
                 super
+                @class_stack = new Stack
+                @method_stack = new Stack
+
+        visitCallExpression: (n) ->
+                if n.callee.type is syntax.Identifier and n.callee.name is "super"
+                        n.callee =
+                                type: syntax.MemberExpression
+                                object:
+                                        type: syntax.MemberExpression
+                                        object: create_identifier "%super"
+                                        property: create_identifier "prototype"
+                                        computed: false
+                                property: @method_stack.top
+                                computed: false
+                n.arguments = @visitArray n.arguments
+                n
+
+        visitNewExpression: (n) ->
+                if n.callee.type is syntax.Identifier and n.callee.name is "super"
+                        n.callee = create_identifier "%super"
+                n.arguments = @visitArray n.arguments
+                n
+
+        visitIdentifier: (n) ->
+                return create_identifier "%super" if n.name is "super"
+                n

         visitClassDeclaration: (n) ->
+                # we visit all the functions defined in the class so that 'super' is replaced with '%super'
+                @class_stack.push n
+
+                # XXX this push/pop should really be handled in @visitMethodDefinition
+                for class_element in n.body.body
+                        @method_stack.push class_element.key
+                        class_element.value = @visit class_element.value
+                        @method_stack.pop
+
+                @class_stack.pop()
+
                 class_init_iife_body = []

                 [properties, methods, sproperties, smethods] = @gather_members n


Fix trigger-happy autocomplete

Fix trigger-happy autocomplete

--- lib/completion-manager.coffee
+++ lib/completion-manager.coffee
@@ -24,7 +24,12 @@
     #ref_rx = /\\(?:eq|page|v|V|auto|name|c|C|cpage)?ref\{/
     ref_rx_rev = /^\{fer(?:qe|egap|v|V|otua|eman|c|C|egapc)?/
     #cite_rx = /\\cite[a-z\*]*?(?:\[.*?\]){0,2}\{/
-    cite_rx_rev = /^([^{},]*)(?:,[^{},]*)*\{(?:\].*?\[){0,2}([a-zX*]*?)etic\\/
+    # Avoid trigger-happy autocomplete: only match (and capture) text
+    # after commas or braces *if* invoked from keybinding
+    if keybinding
+      cite_rx_rev = /^([^{},]*)(?:,[^{},]*)*\{(?:\].*?\[){0,2}([a-zX*]*?)etic\\/
+    else
+      cite_rx_rev = /^(?:,[^{},]*)*\{(?:\].*?\[){0,2}([a-zX*]*?)etic\\/

     current_point = te.getCursorBufferPosition()
     initial_point = [current_point.row, Math.max(0,current_point.column - max_length)]


update dobi cache:warm

update dobi cache:warm

--- lib/cli.coffee
+++ lib/cli.coffee
@@ -243,170 +243,308 @@

   # clear the cache for a site
   when 'cache:warm'
-    domain = args[0]
-    errors = []
-    script_urls = []
-    X_CACHE = {}
-    scripts_loaded = 0
-    sites_parsed = 0
-
-    # check arguments
-    exit "must specify domain" unless domain
-
-    # add http and sitemap
-    domain = "http://#{domain}"
-
-    # get site
-    log 'loading sitemap'
-
-    # get fn
-    get = (url, next) -> request {method: 'GET', url}, next
-
-    cacheCount = (x_cache, type) ->
-      X_CACHE[type] ?= {}
-      X_CACHE[type].hit ?= 0
-      X_CACHE[type].miss ?= 0
-      switch x_cache
-        when 'HIT'
-          X_CACHE[type].hit++
-        when 'MISS'
-          X_CACHE[type].miss++
-
-    # queue
-    site_queue = async.queue ((task, next) ->), 10
-
-    loadSitemap = (next) ->
-      get "#{domain}/sitemap.xml", (err, resp, body) ->
-        return next err if err
-        if resp.statusCode < 200 or resp.statusCode > 302
-          return next "bad #{body}"
-        next null, body
-
-    parseXML = (body, next) ->
-      XMLparser.parseString body, (err, result) ->
-        return next err if err
-        sites = (loc for {loc} in result.urlset.url)
-        log "#{sites.length} site locations retrieved"
-        next null, sites
-
-    loadSites = (sites, done) ->
-      scripts = []
-
-      # queue
-      site_queue = async.queue (({site}, next) ->
-        get site, (err, resp, body) ->
-          if resp.statusCode < 200 or resp.statusCode > 302 or err
-            if body is 'Unauthorized'
-              log "UNAUTHORIZED: #{site}".yellow
-            else
-              log "ERROR: #{body}. skipping #{site}.".red
-            return next()
-
-          log "HTML RETRIEVED: #{site}"
-          {headers} = resp
-          x_cache = headers['x-cache']
-          cacheCount x_cache, 'HTML'
-
-          sites_parsed++
-          parser = new htmlparser.Parser {
-            onopentag: (name, attribs) ->
-              switch attribs.type
-                when 'text/css'
-                  url = attribs.href
-                  if url and url not in script_urls
-                    script = {type: 'CSS', url, site}
-                    script_urls.push url
-                    scripts.push script
-                when 'text/javascript'
-                  url = attribs.src
-                  if url and url not in script_urls
-                    script = {type: 'JS', url, site}
-                    script_urls.push url
-                    scripts.push script
-            onend: ->
-              next()
-          }
-          parser.write body
-          parser.end()
-      ), 10
-
-      (site_queue.push {site}) for site in sites
-      site_queue.drain = -> done null, scripts
-
-    loadScripts = (scripts, done) ->
+    DOMAIN = args[0]
+    exit "must specify domain" unless DOMAIN
+
+    # connect to DB
+    connect (user, db) ->
+
+      DOMAIN = "http://#{DOMAIN}"
+      RAW_DATA = {
+        CACHE: {}
+        ERRORS: []
+        SCRIPTS_LOADED: 0
+        SERVERS: {}
+        SITES_PARSED: 0
+        TIME: {}
+      }
+
+      # regular expresssions
       DOMAINtest = /^\/[^\/]/gi
       HTTPtest = /^https*/gi
-
-      script_queue = async.queue (({script}, next) ->
-        {url, type, site} = script
-        if url.match DOMAINtest
-          url = "#{domain}#{url}"
-        else if not url.match HTTPtest
-          url = "http:#{url}"
-        get url, (err, resp, body) ->
-          if err or resp?.statusCode < 200 or resp?.statusCode > 302
-            errors.push {
-              error: err
-              body: body
-              site: site
-              url: url
-              response: resp?.statusCode
+      LT3test = /lessthan3.com/gi
+      PKGtest = /(\/pkg\/.+\/.+)\//gi
+
+
+      # get fn
+      get = (url, next) ->
+        time_start = Date.now()
+        request {method: 'GET', url}, (err, resp, body) ->
+          resp?.headers?.time = Date.now() - time_start
+          next err, resp, body
+
+      cacheCount = (headers, type) ->
+        x_cache = headers['x-cache']
+        x_served_by = headers['x-served-by']
+        request_time = headers['time']
+
+        RAW_DATA.CACHE[type] ?= {}
+        RAW_DATA.CACHE[type].hit ?= 0
+        RAW_DATA.CACHE[type].miss ?= 0
+
+        RAW_DATA.SERVERS[x_served_by] ?= {}
+        RAW_DATA.SERVERS[x_served_by].hit ?= 0
+        RAW_DATA.SERVERS[x_served_by].miss ?= 0
+
+        RAW_DATA.TIME[type] ?= {}
+        RAW_DATA.TIME[type].hit ?= []
+        RAW_DATA.TIME[type].miss ?= []
+
+        switch x_cache
+          when 'HIT'
+            RAW_DATA.CACHE[type].hit++
+            RAW_DATA.SERVERS[x_served_by].hit++
+            RAW_DATA.TIME[type].hit.push request_time
+          when 'MISS'
+            RAW_DATA.CACHE[type].miss++
+            RAW_DATA.SERVERS[x_served_by].miss++
+            RAW_DATA.TIME[type].miss.push request_time
+
+      # get sitemap
+      loadSitemap = (domain, next) ->
+        log 'loading sitemap'
+        get "#{domain}/sitemap.xml", (err, resp, body) ->
+          return next err if err
+          if resp.statusCode < 200 or resp.statusCode > 302
+            return next "bad #{body}"
+          next null, body
+
+      parseXML = (body, next) ->
+        XMLparser.parseString body, (err, result) ->
+          return next err if err
+          sites = (loc for {loc} in result.urlset.url)
+          return next() unless sites
+          log "#{sites.length} site locations retrieved"
+          next null, sites
+
+      getSiteDomains = (sites, done) ->
+        SLUGS = for site in sites
+          slug_test = new RegExp "#{DOMAIN}/(.+)", "gi"
+          slug = slug_test.exec(site)?[1]
+          slug if slug
+
+        db_sites = db.collection 'sites'
+
+        getDomains = (done) ->
+          DOMAINS = []
+          async.eachSeries SLUGS, ((slug, next) ->
+            db_sites.findOne {'slug': slug}, (err, result) ->
+              return next() if result is null
+              url = result.val().settings?.domain?.url
+              return next() unless url
+              url = "http://#{url}" unless url.match HTTPtest
+              if url not in sites and not url.match LT3test
+                DOMAINS.push url
+                log "ADDING: #{url}"
+              next()
+          ), (err) ->
+            done null, DOMAINS
+
+        loadDomainSitemap = (domains, done) ->
+          SITES = []
+          async.eachSeries domains, ((domain, next) ->
+            async.waterfall [
+              (_next) -> loadSitemap domain, _next
+              (body, _next) -> parseXML body, _next
+            ], (err, results) ->
+              return next() unless results
+              SITES.push result for result in results
+              next()
+          ), (err) ->
+            done null, SITES
+
+
+        async.waterfall [
+          (next) -> getDomains next
+          (domains, next) -> loadDomainSitemap domains, next
+        ], (err, new_sites) ->
+          sites.push site for site in new_sites
+          done null, sites
+
+
+      loadSites = (sites, done) ->
+        SCRIPTS = []
+        SCRIPT_URLS = []
+
+        async.eachSeries sites, ((site, next) ->
+          get site, (err, resp, body) ->
+            if resp.statusCode < 200 or resp.statusCode > 302 or err
+              switch body
+                when 'Unauthorized'
+                  log "UNAUTHORIZED: #{site}".yellow
+                else
+                  log "ERROR: #{body}. skipping #{site}.".red
+              return next()
+
+            log "HTML RETRIEVED: #{site}"
+            cacheCount resp.headers, 'HTML'
+
+            RAW_DATA.SITES_PARSED++
+            parser = new htmlparser.Parser {
+              onopentag: (name, attribs) ->
+                switch attribs.type
+                  when 'text/css'
+                    url = attribs.href
+                    if url and url not in SCRIPT_URLS
+                      script = {type: 'CSS', url, site}
+                      SCRIPT_URLS.push url
+                      SCRIPTS.push script
+                  when 'text/javascript'
+                    url = attribs.src
+                    if url and url not in SCRIPT_URLS
+                      script = {type: 'JS', url, site}
+                      SCRIPT_URLS.push url
+                      SCRIPTS.push script
+              onend: ->
+                next()
             }
-            return next()
-
-          {headers} = resp
-          x_cache = headers['x-cache']
-          cacheCount x_cache, type
-
-          scripts_loaded++
+            parser.write body
+            parser.end()
+        ), ->
+          done null, SCRIPTS
+
+
+      getPackageScripts = (scripts, next) ->
+        pkg_scripts = []
+        for {url, type, site} in scripts
+          continue unless type is 'CSS'
+          pkg = PKGtest.exec(url)?[1]
+          continue unless pkg
+          pkg_JS = "#{pkg}/main.js"
+          if pkg_JS not in pkg_scripts
+            scripts.push {site, type: 'JS', url: pkg_JS}
+            pkg_scripts.push pkg_JS
+
+        next null, scripts
+
+      loadScripts = (scripts, done) ->
+
+        async.eachSeries scripts, ((script, next) ->
+          {url, type, site} = script
+          if url.match DOMAINtest
+            url = "#{DOMAIN}#{url}"
+          else unless url.match HTTPtest
+            url = "http:#{url}"
+          get url, (err, resp, body) ->
+            if err or resp?.statusCode < 200 or resp?.statusCode > 302
+              RAW_DATA.ERRORS.push {
+                error: err
+                body: body
+                site: site
+                url: url
+                response: resp?.statusCode
+              }
+              return next()
+
+            log "#{type} RETRIEVED: #{site}"
+            cacheCount resp.headers, type
+
+            RAW_DATA.SCRIPTS_LOADED++
+            next()
+        ), done
+
+      compileErrors = (next) ->
+        {ERRORS} = RAW_DATA
+        return next() unless ERRORS.length > 0
+        err_format = "```js\n#{JSON.stringify ERRORS, null, 2}\n```"
+        clipboard.copy err_format, ->
+          log "Errors copied to clipboard"
           next()
-      ), 10
-
-      (script_queue.push {script}) for script in scripts
-      script_queue.drain = -> done()
-
-    async.waterfall [
-      (next) -> loadSitemap next
-      (body, next) -> parseXML body, next
-      (sites, next) -> loadSites sites, next
-      (scripts, next) -> loadScripts scripts, next
-    ], (err) ->
-      {HTML, CSS, JS} = X_CACHE
-      exit err if err
-      c = console
-
-      cache_data = []
-      for type, {hit, miss} of X_CACHE
-        cache_data.push {type, hit, miss}
-
-      cache_table = columnify cache_data, {
-        columns: ['type', 'hit', 'miss']
-        columnSplitter: ' | '
-      }
-
-      c.log ''
-      log '= = = = = = = = = = = ='
-      log ''
-      log 'CACHE:WARM STATS'
-      log "SITES LOADED: #{sites_parsed}"
-      log "SCRIPTS LOADED: #{scripts_loaded}"
-      if errors
-        log "SCRIPT ERRORS: #{errors.length}".red
-      else
-        log 'SCRIPT ERRORS: 0'
-      c.log '\nCACHE STATS'
-      c.log "#{cache_table}\n"
-
-      if errors
-        errors_formatted = "```js\n#{JSON.stringify errors, null, 2}\n```"
-        clipboard.copy errors_formatted, ->
-          log 'Errors copied to clipboard'
-          log '= = = = = = = = = = = ='
-          exit()
-      else
-        log 'No errors!'
-        log '= = = = = = = = = = = ='
-        exit()
+
+      compileData = (next) ->
+
+        data_config = {
+          'cache:warm':
+            data: {
+              'SITES LOADED': RAW_DATA.SITES_PARSED
+              'SCRIPTS LOADED': RAW_DATA.SCRIPTS_LOADED
+              'SCRIPT ERRORS': RAW_DATA.ERRORS.length
+            }
+            format:
+              columns: ['metric', 'count']
+              columnSplitter: ' | '
+            prepare: (data) ->
+              result = []
+              for metric, count of data
+                result.push {metric, count}
+              return result
+
+          cache:
+            data: RAW_DATA.CACHE
+            format:
+              columns: ['type', 'hit', 'miss']
+              columnSplitter: ' | '
+            prepare: (data) ->
+              result = []
+              for type, {hit, miss} of data
+                result.push {type, hit, miss}
+              return result
+
+          server:
+            data: RAW_DATA.SERVERS
+            format:
+              columns: ['server', 'hit', 'miss']
+              columnSplitter: ' | '
+            prepare: (data) ->
+              result = []
+              for server, {hit, miss} of data
+                continue unless server isnt 'undefined'
+                result.push {server, hit, miss}
+              result = result.sort (a, b) ->
+                return 1 if a.server > b.server
+                return -1 if a.server < b.server
+                return 0
+              return result
+
+          time: {
+            data: RAW_DATA.TIME
+            format:
+              columns: ['type', 'hit', 'miss']
+              columnSplitter: ' | '
+            prepare: (data) ->
+              result = []
+              timeAvg = (times) ->
+                times ?= []
+                average = 0
+                average += time for time in times
+                average /= times.length if average > 0
+                return "#{Math.floor average}ms"
+              for type, {hit, miss} of data
+                metrics = {type}
+                for TYPE in ['hit', 'miss']
+                  metrics[TYPE] = timeAvg data[type][TYPE]
+                result.push metrics
+              return result
+          }
+        }
+
+        metrics = []
+        for metric, methods of data_config
+          {data, format, prepare} = methods
+          metrics.push {
+            title: "#{metric.toUpperCase()} STATS"
+            table: columnify prepare(data), format
+          }
+
+        next null, metrics
+
+      async.waterfall [
+        (next) -> loadSitemap DOMAIN, next
+        (body, next) -> parseXML body, next
+        (sites, next) -> getSiteDomains sites, next
+        (sites, next) -> loadSites sites, next
+        (scripts, next) -> getPackageScripts scripts, next
+        (scripts, next) -> loadScripts scripts, next
+        (next) -> compileErrors next
+        (next) -> compileData next
+      ], (err, metrics) ->
+        exit err if err
+        border = '= = = = = = = = = = = = = = = = = = = = = = = ='
+        output = ['CACHE:WARM COMPLETE', border, '']
+        for {title, table} in metrics
+          output = output.concat([title, table, ''])
+        log output.join '\n'
+        process.exit()

   # clone a site
   when 'clone'


Inherit from User

Inherit from User

--- models/current_user.coffee
+++ models/current_user.coffee
@@ -12,11 +12,10 @@
 Following           = require '../components/follow_button/collection.coffee'
 ABM                 = require 'artsy-backbone-mixins'
 mediator            = require '../lib/mediator.coffee'
-Geo                 = require './mixins/geo.coffee'
+User                = require './user.coffee'

-module.exports = class CurrentUser extends Backbone.Model
+module.exports = class CurrentUser extends User
   _.extend @prototype, ABM.CurrentUser(sd.API_URL)
-  _.extend @prototype, Geo

   url: ->
     "#{sd.API_URL}/api/v1/me"


id -> class( img_upload )

id -> class( img_upload )

--- src/chat_room_view.coffee
+++ src/chat_room_view.coffee
@@ -51,7 +51,7 @@

         <textarea class="grw" placeholder="Enter Message..."></textarea>
-
+

     ')


Quick Fix

Quick Fix

--- modules/player/util.coffee
+++ modules/player/util.coffee
@@ -182,7 +182,7 @@
       thumbnail: info.thumbnail
       radioStream: info.isRadioStream or false
       time: info.startAt if isFinite(info.duration) and info.duration > 0
-      videoPath: info.formats.find((f)-> f.width and f.width >= 480) or info.url
+      videoPath: (info.formats.find((f)-> f.width and f.width >= 480) or info).url
       duration
       filters
     }, playlist, not play)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/engine_selector.coffee
+++ test/engine_selector.coffee
@@ -18,7 +18,7 @@
     x: 5
     y: 10

-  engine.each (obj, index) -
+  engine.each (obj, index) ->

 test "#find", ->
   engine = Engine()


Add support for onCompile in plugins.

Add support for onCompile in plugins.

--- src/commands/watch.coffee
+++ src/commands/watch.coffee
@@ -29,9 +29,10 @@
     , []

 class BrunchWatcher
-  constructor: (@persistent, @options, @onCompile) ->
+  constructor: (@persistent, @options, @_onCompile) ->
     params = {}
     params.minify = yes if options.minify
+    params.persistent = persistent
     if persistent
       params.server = {}
       params.server.run = yes if options.server
@@ -83,6 +84,14 @@
         .on('unlink', @removeFromFileList)
         .on('error', logger.error)

+  onCompile: (result) =>
+    @_onCompile result
+    @plugins
+      .filter (plugin) ->
+        typeof plugin.onCompile is 'function'
+      .forEach (plugin) ->
+        plugin.onCompile result
+
   compile: =>
     paths = @config.paths
     fs_utils.write @fileList, @config, @plugins, (error, result) =>
@@ -91,7 +100,7 @@
         logger.info "compiled."
         logger.debug "compilation time: #{Date.now() - @start}ms"
         @watcher.close() unless @persistent
-        @onCompile null, result
+        @onCompile result

   watch: ->
     @initServer()


Fix server.

Fix server.

--- src/commands/watch.coffee
+++ src/commands/watch.coffee
@@ -81,6 +81,7 @@
     params.server = {}
     params.server.run = yes if options.server
     params.server.port = options.port if options.port
+  params

 class BrunchWatcher
   constructor: (@persistent, @options, @_onCompile) ->


Revert `dependencies` special casing for `chain` func

--- lib/chain.coffee
+++ lib/chain.coffee
@@ -15,20 +15,19 @@
     return cb()

   # stack iterator
-  callNext = (index, args, dependencies) ->
+  callNext = (index, args) ->

     # exit condition
     unless index < stack.length
       return cb null, args

     # run next service
-    stack[index] args, ((err, results) ->
+    stack[index] args, (err, results) ->
       results ||= {}
       unless getType(results) is 'Object'
         return cb (new Error "#{stack[index].serviceName or serviceName} must return an object."), {results: results}
       return cb err, results if err
-      callNext index + 1, results, dependencies
-    ), dependencies
+      callNext index + 1, results

   # begin execution
-  callNext 0, input, dependencies
+  callNext 0, input


r3: normalized dir in v_dist_point_to_rect

--- engine/geometry_utils/r3.coffee
+++ engine/geometry_utils/r3.coffee
@@ -47,9 +47,7 @@
     # p is a point
     # rp is a point of the rect
     # dir is the director vector of the rect
-
-    vec3.normalize dir, dir
-    vec3.cross out, vec3.sub(v1,p,rp), dir
+    vec3.cross out, vec3.sub(v2,p,rp), vec3.normalize v1, dir
     return out


log settings on startup

log settings on startup

--- services/clsi/config/settings.defaults.coffee
+++ services/clsi/config/settings.defaults.coffee
@@ -46,3 +46,4 @@
 	module.exports.path.sandboxedCompilesHostDir = process.env["COMPILES_HOST_DIR"]

 	module.exports.path.synctexBinHostPath = process.env["SYNCTEX_BIN_HOST_PATH"]
+console.log module.exports


Always specify XPath index explicitly.

Always specify XPath index explicitly.

This is preparation for dealing effectively with changing content, and
addresses the following problem. Say the following [[annotation]] is made:

Hi there, this is an [[emphasised]] text

This will (currently) be serialized as [('/p/em', 3), ('/p/em', 13)]. If I
modify the document to become

I broke it.

Hi there, this is an emphasised text

Then the deserializer will actually work, because '/p/em' (with unspecified
indices) is unambiguous in the document. But this is really just an accident,
and is inconsistent with the fact that if the original document were

Yet another paragraph

Hi there, this is an [[emphasised]] text

the annotation would be serialized as [('/p[2]/em', 3), ('/p[2]/em', 13)] and
an identical transformation of the document, to

I broke it.

Yet another paragraph

Hi there, this is an [[emphasised]] text

will now cause the deserializer to break, because '/p[2]/em' doesn't match
anything.

I think these two cases should behave identically, and so $(...).xpath() now
emits explicit indices all the time.

--- test/spec/range_spec.coffee
+++ test/spec/range_spec.coffee
@@ -45,9 +45,9 @@

     it "serialize() returns a serialized range", ->
       seri = r.serialize(fix())
-      expect(seri.start).toEqual("/p/strong")
+      expect(seri.start).toEqual("/p[1]/strong[1]")
       expect(seri.startOffset).toEqual(13)
-      expect(seri.end).toEqual("/p/strong")
+      expect(seri.end).toEqual("/p[1]/strong[1]")
       expect(seri.endOffset).toEqual(27)
       expect(seri instanceof Range.SerializedRange).toBeTruthy()


Exit loop in NotificationIssue::findSimilarIssues() when match is found

--- lib/notification-issue.coffee
+++ lib/notification-issue.coffee
@@ -27,7 +27,10 @@
           if data.items?
             issues = {}
             for issue in data.items
-              issues[issue.state] = issue if issue.title.indexOf(@getIssueTitle()) > -1 and not issues[issue.state]?
+              if issue.title.indexOf(@getIssueTitle()) > -1 and not issues[issue.state]?
+                issues[issue.state] = issue
+                break
+
             return resolve(issues) if issues.open? or issues.closed?
           resolve(null)
         error: -> resolve(null)


Change param -> params

Change param -> params

--- lib/passport/callbacks.coffee
+++ lib/passport/callbacks.coffee
@@ -108,7 +108,7 @@
   # No errors—create the user from the access token.
   if not err
     done null, new opts.CurrentUser accessToken: res.body.access_token
-  else if param.provider is 'twitter' and msg.match('no account linked')?
+  else if params.provider is 'twitter' and msg.match('no account linked')?
     err = new Error "No Twitter account found, please sign up"
     done err
   # If there's no user linked to this account, create the user via the POST


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/color.coffee
+++ test/color.coffee
@@ -116,8 +116,6 @@
   equals rgbColor.b(), 210
   equals rgbColor.a(), 0.3

-
-
 test "Color should equal colors with the same rbga values", ->
   color1 = Color(4, 20, 100)
   color2 = Color('rgb(4, 20, 100)')


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/color.coffee
+++ test/color.coffee
@@ -154,6 +154,16 @@
   equalEnough color.g, 43
   equalEnough color.b, 100
   equalEnough color.a, 0.32
+
+test "accepts a color object", ->
+  color = Color(23, 54, 49, 1)
+
+  newColor = Color(color)
+
+  equal newColor.r, 23
+  equal newColor.g, 54
+  equal newColor.b, 49
+  equal newColor.a, 1

 test "invalid color throws error", ->
   raises ->


Add couple of tests

Add couple of tests

--- test/testJsonPointer.coffee
+++ test/testJsonPointer.coffee
@@ -2,6 +2,7 @@
 jsonPointer = require "../src/jsonPointer"
 chai = require "chai"
 chai.should()
+expect = chai.expect

 describe "jsonPointer", () ->
@@ -42,3 +43,34 @@
         actual.should.be.deep.equal expected

       check(expression, expected) for expression, expected of specExamples
+
+
+    it "returns undefined if value is not found", () ->
+      target =
+        foo: "bar"
+
+      targetAsString = JSON.stringify target
+
+      actual = jsonPointer.get targetAsString, "/baz/0"
+      expect(actual).to.be.undefined
+
+
+    describe "validates pointer and", () ->
+      target =
+        foo: "bar"
+      targetAsString = JSON.stringify target
+      getTestFunction = (pointer) ->
+        () -> jsonPointer.get targetAsString, pointer
+
+      it "throws an error if pointer is not valid", () ->
+        invalidPointers = ["a", "/01"]
+        testFunctions = invalidPointers.map getTestFunction
+
+        expect(f).to.throw Error for f in testFunctions
+
+
+      it "does not throw an error if pointer is valid", () ->
+        validPointers = ["", "/", "//", "/a", "/0", "/10", "/a/0", "/1/a"]
+        testFunctions = validPointers.map getTestFunction
+
+        expect(f).to.not.throw Error for f in testFunctions


peillis/helpapp#3455 two more simple controller tests

--- test/test_controller.coffee
+++ test/test_controller.coffee
@@ -162,6 +162,38 @@

       controller.refresh() # search wigh color: ['Azul', 'Rojo'] and brand: ['Nike']

+    it 'nextPage redo the search with next page', (done) ->
+      controller = new doofinder.Controller client_mock, [widget_mock]
+      # when first search done, try nextPage
+      controller.bind 'df:search', ()->
+        client_mock.search = (query, params, cb) ->
+          # same query
+          params.query.should.be.equal 'silla'
+          # increased page
+          params.page.should.be.equal 2
+          done()
+
+        # next page!
+        controller.nextPage()
+
+      # first search
+      controller.search 'silla'
+
+    it 'getPage redo the search with any specified page', (done) ->
+      controller = new doofinder.Controller client_mock, [widget_mock]
+      # when first search done, try getPage
+      controller.bind 'df:search', ()->
+        client_mock.search = (query, params, cb) ->
+          params.query.should.be.equal 'silla'
+          params.page.should.be.equal 22
+          done()
+
+        # get page!
+        controller.getPage 22
+
+      # first search
+      controller.search 'silla'
+

     it 'when removing terms filters, filters params change', (done) ->
       controller = new doofinder.Controller client_mock, [widget_mock]


fixed trailing colon in group url

fixed trailing colon in group url

--- client/landing/app/Applications/Groups.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Groups.kdapplication/AppController.coffee
@@ -925,7 +925,7 @@
     group.fetchMembershipPolicy (err, policy)=>
       return new KDNotificationView title: 'An error occured, however your group has been created!' if err

-      port = if location.port isnt 80 then ":#{location.port}" else ''
+      port = if location.port then ":#{location.port}" else ''
       groupUrl = "#{location.protocol}//#{location.hostname}#{port}/#{group.slug}"

       if group.privacy is 'public'


locationRange is an object now, not an array

--- src/trix/models/document.coffee
+++ src/trix/models/document.coffee
@@ -69,15 +69,15 @@

         callback(block, textRange, index)

-  insertTextAtLocationRange: edit (text, range) ->
-    @blockList = @blockList.editObjectAtIndex range.index, (block) ->
-      block.copyWithText(block.text.insertTextAtPosition(text, range.position))
+  insertTextAtLocationRange: edit (text, locationRange) ->
+    @blockList = @blockList.editObjectAtIndex locationRange.index, (block) ->
+      block.copyWithText(block.text.insertTextAtPosition(text, locationRange.position))

   removeTextAtLocationRange: edit (locationRange) ->
     range = @rangeFromLocationRange(locationRange)
     blockList = @blockList.removeObjectsInRange(range)

-    leftIndex = locationRange[0].index
+    leftIndex = locationRange.index
     rightIndex = leftIndex + 1
     leftBlock = blockList.getObjectAtIndex(leftIndex)
     rightBlock = blockList.getObjectAtIndex(rightIndex)
@@ -89,9 +89,9 @@

     @blockList = blockList

-  replaceTextAtLocationRange: edit (text, range) ->
-    @removeTextAtLocationRange(range)
-    @insertTextAtLocation(text, range[0])
+  replaceTextAtLocationRange: edit (text, locationRange) ->
+    @removeTextAtLocationRange(locationRange)
+    @insertTextAtLocationRange(text, locationRange)

   addAttributeAtLocationRange: edit (attribute, value, locationRange) ->
     @eachBlockInLocationRange locationRange, (block, range, index) =>
@@ -115,14 +115,14 @@
     @blockList.replaceBlockList(document.blockList)
     @delegate?.didEditDocument?(this)

-  getCommonAttributesAtLocationRange: (range) ->
-    if range.isCollapsed()
-      @getCommonAttributesAtLocation(range.start)
+  getCommonAttributesAtLocationRange: (locationRange) ->
+    if locationRange.isCollapsed()
+      @getCommonAttributesAtLocation(locationRange.start)
     else
       textAttributes = []
       blockAttributes = []

-      @eachBlockAtLocationRange range, (block, textRange) ->
+      @eachBlockAtLocationRange locationRange, (block, textRange) ->
         textAttributes.push(block.text.getCommonAttributesAtRange(textRange))
         blockAttributes.push(block.getAttributes())

@@ -173,8 +173,8 @@
         block.copyWithText(text.resizeAttachmentToDimensions(attachment, dimensions))

   rangeFromLocationRange: (locationRange) ->
-    leftPosition = @blockList.findPositionAtIndexAndOffset(locationRange[0].index, locationRange[0].position)
-    rightPosition = @blockList.findPositionAtIndexAndOffset(locationRange[1].index, locationRange[1].position)
+    leftPosition = @blockList.findPositionAtIndexAndOffset(locationRange.start.index, locationRange.start.position)
+    rightPosition = @blockList.findPositionAtIndexAndOffset(locationRange.end.index, locationRange.end.position)
     [leftPosition, rightPosition]

   toJSON: ->


Don't inherit width and height attributes

--- src/trix/text_controller.coffee
+++ src/trix/text_controller.coffee
@@ -115,15 +115,17 @@
       @currentAttributes = @text.getCommonAttributesAtRange(selectedRange)
       @notifyDelegateOfCurrentAttributesChange()
     else if (position = @getPosition())?
+      # TODO: Consider whitelisting allowed current attributes from previous position
       @currentAttributes = @text.getAttributesAtPosition(position - 1)
       @adjustCurrentAttributesForPosition(position)
       @notifyDelegateOfCurrentAttributesChange()

   adjustCurrentAttributesForPosition: (position) ->
-    # If the cursor is positioned after an href, don't consider it current.
-    if href = @currentAttributes["href"]
-      if href isnt @text.getAttributesAtPosition(position)["href"]
-        delete @currentAttributes["href"]
+    # Don't consider these attributes current when the cursor is positioned after them
+    for attribute in ["href", "width", "height"]
+      if currentAttribute = @currentAttributes[attribute]
+        if currentAttribute isnt @text.getAttributesAtPosition(position)[attribute]
+          delete @currentAttributes[attribute]

   notifyDelegateOfCurrentAttributesChange: ->
     @delegate?.textControllerDidChangeCurrentAttributes?(@currentAttributes)


Add support for collections, restructure the graft function a bit.

--- lib/graft.coffee
+++ lib/graft.coffee
@@ -63,13 +63,10 @@
       })

 The mechanism is straightforward: if you pass an array as the second parameter,
-the array is checked: if the objects inside it are Functions, each Function is
-invoked in turn with a new clone of the matched element(s), then the results
-are gathered up and put into the DOM in the matched elements' place. If the
-objects inside it are plain-jane vanilla Objects, as above, then each of those
-Objects is passed to graft for grafting purposes with a new clone of the
-matched element(s), and again the results are gathered and dropped into the
-DOM.
+each object in the array is passed to graft in turn, each with a new clone of
+the matched element(s). Then, these elements are gathred up and put into the
+DOM instead of the matched element. This means you provide an array of
+functions, if you prefer, or an object, as above, or a string, etc.

 The goodness is good, no?

@@ -115,24 +112,40 @@

     # If the selector is an object, it means we're grafting sub-selectors of
     # the top-level object.
-    if typeof(selector) == 'object'
+    if typeof selector == 'object'
       $base.graft subselector, generator for subselector, generator of selector
     else
-      # Functions get passed the result of the selector.
-      if typeof(generators) == 'function'
-        generators $base.find(selector)
-      # Strings are text for replacing the text of the selector matches.
-      else if typeof(generators) == 'string'
-        $base.find(selector).text(generators)
-      # If we get a jQuery object (identified by the selector property), we
-      # replace the selector matches' html contents with that object's
-      # contents.
-      else if typeof(generators) == 'object' && generators.selector?
-        $base.find(selector).html(generators)
-      # If we get a non-jQuery object, we just run graft all the properties as
-      # selectors with their values as generators.
-      else if typeof(generators) == 'object'
-        $base.find(selector).graft subselector, generator for subselector, generator of generators
+      switch typeof generators
+        # Functions get passed the result of the selector.
+        when 'function'
+          generators $base.find(selector)
+        # Strings are text for replacing the text of the selector matches.
+        when 'string' or 'number'
+          $base.find(selector).text(generators)
+        when 'object'
+          # If we get a jQuery object (identified by the selector property), we
+          # replace the selector matches' html contents with that object's
+          # contents.
+          if generators.selector?
+            $base.find(selector).html(generators)
+          # If we get an array, identified by a map property, we iterate
+          # through it, grafting its components to clones of the base, then
+          # returning them as one group.
+          # FIXME This will cause issues in older browsers.
+          else if generators.map?
+            generators.map (generator) ->
+              generatorType = typeof generator
+              if generatorType == 'object'
+                $base = $base.clone().graft generator
+              else if generatorType == 'string' || generatorType == 'number'
+                $base = $base.clone().text generator
+              else if generatorType == 'function'
+                $base = $base.each generator
+
+          # If we get a non-jQuery object, we just run graft all the properties as
+          # selectors with their values as generators.
+          else
+            $base.find(selector).graft subselector, generator for subselector, generator of generators

     # If we are a template, append the result to the original template's
     # parent.


Accept mongodb host in queue spec

Accept mongodb host in queue spec

--- test/queue-worker-spec.coffee
+++ test/queue-worker-spec.coffee
@@ -10,6 +10,10 @@

 describe 'QueueWorker', ->
   beforeEach ->
+    mongoHost = process.env.MONGODB_HOST ? 'localhost'
+    mongoPort = process.env.MONGODB_PORT ? '27017'
+    @databaseUri = "#{mongoHost}:#{mongoPort}/meshblu-core-test"
+
     @clientId = uuid.v1()
     @cacheClientId = uuid.v1()
     @client = _.bindAll redis.createClient @clientId
@@ -17,7 +21,7 @@
     @datastoreFactory =
       build: (collection) =>
         new Datastore
-          database: 'meshblu-core-test'
+          database: @databaseUri
           collection: collection

     @cacheFactory =
@@ -103,7 +107,7 @@
     describe 'when called with an CheckToken job', ->
       beforeEach (done) ->
         datastore = new Datastore
-          database: 'meshblu-core-test'
+          database: @databaseUri
           collection: 'devices'

         record =


Added test for prev commit

Added test for prev commit

--- test/index.coffee
+++ test/index.coffee
@@ -340,6 +340,14 @@
         lock1.releaseLock().on 'released', (ld) ->
           lock1.renewLock().on 'error', (e) ->
             assert.throws (() -> throw e), /cannot renew an unheld lock/
+            done()
+
+      it "should fail to renew a non-expiring lock", (done) ->
+        l = Lock id, lockColl
+        l.obtainReadLock().on 'locked', (ld) ->
+          assert ld?
+          l.renewLock().on 'error', (e) ->
+            assert.throws (() -> throw e), /cannot renew a non-expiring lock/
             done()

   describe 'waiting for locks', () ->


fixed require calls, fixed relistener use

--- lib/index.coffee
+++ lib/index.coffee
@@ -8,12 +8,8 @@
 # but, it's expected to require early on to discover if they're missing and
 # so they're near the top (easy to find)
 addListeners = require './add-listeners'
-buildRelistener   = require './relistener'
-multiplexor  = require './multiplex'
-eventor      = require './eventor'
-jsonify      = require './jsonify'
-authenticator= require './authenticate-client'
-transformer  = require './transformer'
+readCerts    = require './read-certs'
+relistener = require './relistener'

 module.exports = (builderOptions) ->

@@ -72,10 +68,7 @@

     # 6. unless 'address-in-use' helper is specified to *not* be used, then add it
     # for 'listening' to retry listen() the config'd number of times with config'd delay
-    if isServer and not options.noRelisten
-      options.server = socket
-      socket.on 'listening', buildRelistener options
-      delete options.server
+    if isServer and not options.noRelisten then relistener server:socket

     # 7. all done
     return socket


Fixed query double delete

Fixed query double delete

--- lib/index.coffee
+++ lib/index.coffee
@@ -372,9 +372,9 @@
                     delete docIdx[d.docName]
                     emitter.emit 'remove', results[idx], idx
                     results.splice idx, 1
-                    loop
-                      break unless typeof docIdx[idx] is 'number'
-                      docIdx[idx++]--
+                    while idx < results.length
+                      name = results[idx++].docName
+                      docIdx[name]--

             #if modifies is true and !cachedData?
             # Add document. Not sure how to han


Ignore ready state change event of requests not finished yet

--- lib/index.coffee
+++ lib/index.coffee
@@ -377,12 +377,13 @@
     xhr.open method, url
     xhr.setRequestHeader "Content-type", "application/json;charset=UTF-8"
     xhr.onreadystatechange = =>
-      if xhr.status is 0
-        return # TODO: try again with backoff
+
+      return  if xhr.readyState is 0   # 0: UNSENT
+      return  if xhr.readyState isnt 4 # 4: DONE
+
       if xhr.status >= 400
         @emit 'error', new Error "XHR Error: #{ xhr.status }"

-      return  if xhr.readyState isnt 4
       return  if xhr.status not in [200, 304]

       requests = JSON.parse xhr.response


no tostring

no tostring

--- lib/nap.coffee
+++ lib/nap.coffee
@@ -417,8 +417,9 @@
 gzipPkg = (contents, filename, callback) =>
   file = "#{process.cwd() + @_outputDir + '/'}#{filename}"
   ext = if _.endsWith filename, '.js' then '.jgz' else '.cgz'
-  zlib.gzip contents, (err, output) ->
-    fs.writeFile file + ext, output.toString(), callback
+  outputFilename = file + ext
+  zlib.gzip contents, (err, buf) ->
+    fs.writeFile outputFilename, buf, callback

 # Generate an md5 hash from the filename + filesize of a package.
 # Used to append a fingerprint to pacakge files for cache busting.


Removed debug string

Removed debug string

--- lib/suggestion-builder.coffee
+++ lib/suggestion-builder.coffee
@@ -90,7 +90,6 @@
         return false if tl.match(/^[a-z]$/)
         ts = tl.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&")
         rx=RegExp ts.replace(/\b[a-z]\b/g,'.+'),''
-        console.log(rx)
         rx.test(type)
       .map (s) =>
         word: s.name


closes #333

closes #333

--- src/core/data/time.coffee
+++ src/core/data/time.coffee
@@ -44,7 +44,7 @@
         pp++
       format = d3.locale(locale).timeFormat.multi(format)
     render = sizes vals.map((v) -> format(v)), style
-    if d3.sum(render, (r) -> r.width) < limit
+    if d3.sum(render, (r) -> r.width) < limit or p is periods.indexOf total
       time.format = format
       time.values = vals
       time.sizes  = render


Assert new Upload with awsBucketName parameter

--- test/suite.coffee
+++ test/suite.coffee
@@ -54,6 +54,11 @@

 describe 'Upload', ->
   describe 'constructor', ->
+    it 'should throw error for missing awsBucketName param', ->
+      assert.throws ->
+        new Upload()
+      , /Bucket name can not be undefined/
+
     it 'should set default values if not provided', ->
       client = new Upload 'myBucket'


Only show Reveal in Tree View for atom-text-editor

--- menus/tree-view.cson
+++ menus/tree-view.cson
@@ -94,10 +94,10 @@
     {'label': 'Paste', 'command': 'tree-view:paste'}
   ]

-  'atom-pane .item-views > *:not(.settings-view)': [
+  'atom-pane atom-text-editor': [
     {'label': 'Reveal in Tree View', 'command': 'tree-view:reveal-active-file'}
   ]

-  'atom-pane .tab.active:not([data-type="SettingsView"])': [
+  'atom-pane .tab.active[data-type="TextEditor"]': [
     {'label': 'Reveal in Tree View', 'command': 'tree-view:reveal-active-file'}
   ]


Bind wheel event to mouseWheel for Firefox support

--- core/KDView.coffee
+++ core/KDView.coffee
@@ -36,6 +36,7 @@
     mouseleave    : "mouseLeave"
     mousemove     : "mouseMove"
     mousewheel    : "mouseWheel"
+    wheel         : "mouseWheel"
     mouseover     : "mouseOver"
     contextmenu   : "contextMenu"
     dragstart     : "dragStart"


Fixed setDraggable method of KDView.

Fixed setDraggable method of KDView.

--- core/KDView.coffee
+++ core/KDView.coffee
@@ -579,10 +579,10 @@

       @dragIsAllowed = yes

-      top    = parseInt @$()[0].style.top, 10
-      right  = parseInt @$()[0].style.right, 10
-      bottom = parseInt @$()[0].style.bottom, 10
-      left   = parseInt @$()[0].style.left, 10
+      top    = parseInt (@$()[0].style.top or 0), 10
+      right  = parseInt (@$()[0].style.right or 0), 10
+      bottom = parseInt (@$()[0].style.bottom or 0), 10
+      left   = parseInt (@$()[0].style.left or 0), 10

       @dragState.startX     = event.pageX
       @dragState.startY     = event.pageY


upgrade RedisClient with selective envelopes

--- library/membrane/redis.coffee
+++ library/membrane/redis.coffee
@@ -54,19 +54,36 @@
     # Once inherited from, the inheritee is not abstract anymore.
     @abstract yes

+    # These defintions are the presets available for configuring
+    # the Redis envelope getting functions. Please set the special
+    # class value `REDIS_ENVELOPE` to either one of these values or
+    # to a custom function that will generate/retrieve the Redis
+    # envelope, when necessary. Depending on this, the system will
+    # generate a new connection on the container, if it does not
+    # contain an opened connection yet. The default container is
+    # the kernel preset using the `REDIS_ENVELOPE_KERNEL` value.
+    @REDIS_ENVELOPE_KERNEL = -> return @kernel
+    @REDIS_ENVELOPE_SERVICE = -> @$redis ?= {}
+
     # A hook that will be called prior to unregistering the service
     # implementation. Please refer to this prototype signature for
     # information on the parameters it accepts. Beware, this hook
     # is asynchronously wired in, so consult with `async` package.
     # Please be sure invoke the `next` arg to proceed, if relevant.
+    # This implementation correctly ends Redis connection, if any.
     unregister: (kernel, router, next) ->
-        return next() unless _.isObject kernel.redis
-        {host, port, options} = kernel.redis or Object()
+        @constructor.REDIS_ENVELOPE ?= -> kernel
+        envelope = this.constructor.REDIS_ENVELOPE
+        envelope = try envelope.apply this, arguments
+        return next() unless _.isObject envelope.redis
+        {host, port, options} = envelope.redis or Object()
         message = "Disconnecting from Redis at %s:%s"
-        logger.info message.toString().cyan, host, port
-        try @emit "redis-gone", kernel.redis, kernel
-        try kernel.emit? "redis-gone", kernel.redis
-        try kernel.redis.end(); delete kernel.redis
+        warning = "Latest Redis envelope was not a kernel"
+        logger.info message.toString().red, host, port
+        logger.debug warning.grey unless envelope is kernel
+        try @emit "redis-gone", envelope.redis, envelope
+        try kernel.emit? "redis-gone", envelope.redis, @
+        try envelope.redis.end(); delete envelope.redis
         next.call this, undefined; return this

     # A hook that will be called prior to registering the service
@@ -74,22 +91,28 @@
     # information on the parameters it accepts. Beware, this hook
     # is asynchronously wired in, so consult with `async` package.
     # Please be sure invoke the `next` arg to proceed, if relevant.
+    # This implementation open a new Redis connection, if configed.
     register: (kernel, router, next) ->
-        config = nconf.get "redis" or null
-        return next() unless _.isObject config
-        return next() if _.isObject kernel.redis
-        {host, port, options} = config or Object()
+        @constructor.REDIS_ENVELOPE ?= -> kernel
+        envelope = this.constructor.REDIS_ENVELOPE
+        envelope = envelope.apply this, arguments
+        assert config = nconf.get("redis") or null
+        return next() unless _.isObject config or 0
+        return next() if _.isObject try envelope.redis
+        {host, port, options} = config or new Object()
         assert _.isString(host), "got invalid Redis host"
         assert _.isNumber(port), "git invalid Redis port"
         assert _.isObject(options), "invalid Redis options"
         message = "Connecting to Redis at %s:%s".toString()
         noRedis = "Something has gone wrong, no Redis client"
+        warning = "Latest Redis envelope was not a kernel".grey
         assert spawner = redisio.createClient.bind redisio
-        logger.info message.toString().cyan, host, port
-        kernel.redis = spawner port, host, options
-        assert _.isObject(kernel.redis), noRedis
-        @emit "redis-ready", kernel.redis, kernel
-        kernel.emit "redis-ready", kernel.redis
+        logger.info message.toString().red, host, port
+        logger.debug warning unless envelope is kernel
+        envelope.redis = spawner port, host, options
+        assert _.isObject(envelope.redis), noRedis
+        kernel.emit "redis-ready", envelope.redis, @
+        @emit "redis-ready", envelope.redis, kernel
         next.call this, undefined; return this

     # A hook that will be called prior to instantiating the service
@@ -97,7 +120,11 @@
     # information on the parameters it accepts. Beware, this hook
     # is asynchronously wired in, so consult with `async` package.
     # Please be sure invoke the `next` arg to proceed, if relevant.
+    # This implementation sets the Redis connection access handle.
     instance: (kernel, service, next) ->
+        @constructor.REDIS_ENVELOPE ?= -> kernel
+        envelope = this.constructor.REDIS_ENVELOPE
+        envelope = try envelope.apply this, arguments
         return next undefined if _.has service, "redis"
         ack = "Acquire Redis client handle in %s".grey
         sig = => this.emit "redis-ready", @redis or null
@@ -105,8 +132,8 @@
         mkp = (prop) -> define service, "redis", prop
         dap = -> mkp arguments...; next(); sig(); this
         dap enumerable: yes, configurable: no, get: ->
-            redis = try @kernel.redis or undefined
-            noRedis = "a kernel has no Redis client"
+            redis = try envelope.redis or undefined
+            noRedis = "an envelope has no Redis client"
             identify = try this.constructor.identify()
             try logger.debug ack, identify.underline
             assert _.isObject(redis), noRedis; redis


minor code reordering

minor code reordering

--- library/nucleus/plumbs.coffee
+++ library/nucleus/plumbs.coffee
@@ -192,7 +192,7 @@
     assert headers = request.headers or new Object()
     assert constant = "X-Session-ID".toLowerCase()
     assert unix = moment().unix().toString().bold
-    message = "Running extSession middleware at U=%s"
+    message = "Running ext-session middleware at U=%s"
     logger.debug message.toString(), unix.toString()
     assert _.isFunction next = _.last arguments
     return next() unless enable and enable is yes
@@ -238,7 +238,12 @@
     assert _.isObject(kernel), "no kernel supplied"
     assert _.isObject(request?.headers), noHeaders
     assert _.isObject(response or null), terribles
-    response.accepts = (mimes...) -> # response func
+    assert unix = moment().unix().toString().bold
+    message = "Running accepting middleware at U=%s"
+    logger.debug message.toString(), unix.toString()
+    assert _.isFunction next = try _.last arguments
+    fn = (arg) -> next() unless request.headersSent
+    fn response.accepts = (mimes...) -> # response fn
         handles = (pattern) -> try pattern.test accept
         patternize = (s) -> new RegExp RegExp.escape s
         accept = do -> request?.headers?.accept or ""
@@ -247,11 +252,6 @@
         strings = _.map strings, patternize.bind this
         assert merged = try _.merge(regexps, strings)
         return _.find(merged, handles) or undefined
-    assert unix = moment().unix().toString().bold
-    message = "Running accepting middleware at U=%s"
-    logger.debug message.toString(), unix.toString()
-    assert _.isFunction next = _.last arguments
-    return next() unless request.headersSent

 # A middleware that adds a `redirect` method to the response object.
 # This redirects to the supplied URL with the 302 status code and
@@ -268,7 +268,11 @@
         assert _.isObject(request?.headers), noHeaders
         assert _.isObject(response or null), terribles
         assert _.isFunction next = try _.last arguments
-        assert response.redirect = (url, status) ->
+        assert unix = moment().unix().toString().bold
+        message = "Running redirect middleware at U=%s"
+        logger.debug message.toString(), unix.toString()
+        fn = (arg) -> next() unless request.headersSent
+        fn assert response.redirect = (url, status) ->
             assert to = try url.toString().underline
             assert from = try request.url?.underline
             assert relocated = status or 302 or null
@@ -278,7 +282,3 @@
             response.writeHead relocated, message
             logger.debug redirect.red, from, to
             return try response.end undefined
-        assert unix = moment().unix().toString().bold
-        message = "Running redirect middleware at U=%s"
-        logger.debug message.toString(), unix.toString()
-        return next() unless request.headersSent


minor changes to the plumbing

minor changes to the plumbing

--- library/nucleus/plumbs.coffee
+++ library/nucleus/plumbs.coffee
@@ -79,9 +79,9 @@
     assert _.isNumber(options?.lag), wrongLag
     assert _.isString(options?.reason), wrongReason
     (busy = require "toobusy").maxLag options.lag
-    message = "Setting threshold maximum lag to %s"
-    logger.info message.magenta, "#{options.lag}ms"
-    return (request, response, next) ->
+    message = "Setting threshold maximum lag to %s ms"
+    logger.info message.magenta, "#{options.lag}".bold
+    return do -> (request, response, next) ->
         return next() unless busy() is yes
         response.writeHead 503, options.reason
         return response.end options.reason


logging

logging

--- lib/place.coffee
+++ lib/place.coffee
@@ -59,14 +59,11 @@

 Place.findByName = (name,subkey,callback) ->
   redis.exists (key = subkey+":"+name), (err, exists) =>
-    console.log(exists)
-    console.log(err)
-    console.log("key")
-    console.log(key)
     if exists == 1
+      log.debug "#{key} is a primary key."
       callback(@new(key))
     else
-      log.debug "#{key} is no primary key. trying alternatives"
+      log.debug "#{key} is NO primary key. trying alternatives"
       redis.smembers "geoname:alt:"+name, (err, alts) =>
         alts = (a for a in alts when a.indexOf(subkey) == 0)
         if alts.length == 1
@@ -85,10 +82,10 @@
   log.debug "trying key pattern "+pattern
   redis.keys pattern, (err, keys) =>
     if keys.length == 0
-      log.debug "no key found."
+      log.debug ".. no key."
       callback undefined
     else if keys.length == 1
-      log.debug "found exactly 1 key"
+      log.debug "..exactly 1 key."
       redis.exists keys[0], (err, exists) =>
         if exists == 1
           callback(@new(keys[0]))


DataHover now also passes event as first arg

--- src/cuffs/bindings.coffee
+++ src/cuffs/bindings.coffee
@@ -101,10 +101,10 @@
             [fnName, args] = @attr.split(':')
             argNames = args?.split(',') or []

-            $(@node).submit =>
+            $(@node).bind 'submit', (e)=>
                 fn = context.get fnName, false
-                fn.apply @node, (context.get(argName) for argName in argNames)
-                #context.get(@attr, false)(@node)
+                fn.apply @node, [e].concat (context.get(argName) for argName in argNames)
+

     class DataBind extends Binding
         # Create two way binding of data between elements and the
@@ -192,10 +192,11 @@
         applyContext: (context)->
             getArgs = =>
                 (context.get(arg) for arg in @funcArgs.split ',' when arg.trim())
-            $(@node).mouseenter =>
-                context.get(@funcName, false).apply @node, [true].concat getArgs()
-            $(@node).mouseleave =>
-                context.get(@funcName, false).apply @node, [false].concat getArgs()
+            fn = context.get @funcName, false
+            $(@node).bind 'mouseenter', (e)=>
+                fn.apply this, [e, true].concat getArgs()
+            $(@node).bind 'mouseleave', (e)=>
+                fn.apply this, [e, false].concat getArgs()

     class DataSet extends Binding
         # Sets a certain value on the model. Sometimes useful. Eg:


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/color.coffee
+++ test/color.coffee
@@ -25,6 +25,20 @@
   equals color.b(), 134
   equals color.a(), 0.33

+test "Color should parse length 8 hex string", ->
+  color = Color('ae56f03a')
+  equals color.r(), 255
+  equals color.g(), 255
+  equals color.b(), 255
+  equals color.a(),
+
+test "Color should parse length 8 hex string with leading #", ->
+  color = Color('#ffffff')
+  equals color.r(), 255
+  equals color.g(), 255
+  equals color.b(), 255
+  equals color.a(), 1.0
+
 test "Color should parse length 6 hex string", ->
   color = Color('ffffff')
   equals color.r(), 255


[test] RedisModel##allKeys

[test] RedisModel##allKeys

--- test/index.coffee
+++ test/index.coffee
@@ -159,3 +159,37 @@

         qCleaner(queue).asCallback done

+  describe "##idsAndCountByStuck", ->
+
+  describe "##stuckKeys", ->
+
+  describe "##allKeys", ->
+    it "should return all keys from all queues by default", (done) ->
+      ideally   = errify done
+      bullModel = new BullModel client
+      data      = {name: "testjob"}
+
+      await bullModel.createJobInQueue queuename, data, ideally defer job
+      await bullModel.createJobInQueue "test2",   data, ideally defer job
+      queue  = bullModel.queues[queuename]
+      queue2 = bullModel.queues["test2"]
+
+      await instance.allKeys null, ideally defer keys
+      expect(keys.length).to.equal 2
+
+      qCleaner queue
+        .then -> qCleaner queue2
+        .asCallback done
+
+    it "should return all keys from a specific queue", (done) ->
+      ideally   = errify done
+      bullModel = new BullModel client
+      data      = {name: "testjob"}
+
+      await bullModel.createJobInQueue queuename, data, ideally defer job
+      queue = bullModel.queues[queuename]
+
+      await instance.allKeys queuename, ideally defer keys
+      expect(keys.length).to.equal 1
+
+      qCleaner(queue).asCallback done


Test fix.

Test fix.

--- test/tests.coffee
+++ test/tests.coffee
@@ -111,10 +111,10 @@
         chatServer.close()
         chatServer = null

-    it.skip 'should reject empty user query', (done) ->
+    it 'should reject empty user query', (done) ->
       chatServer = new ChatService { port : port }, null, state
       socket1 = ioClient.connect url1
-      , { query : 'multiplex' : false
+      , { 'multiplex' : false
         , 'reconnection' : false
         , 'transports' : [ 'websocket' ] }
       socket1.on 'loginRejected', ->


Make test for exclude globs run under windows

--- test/tests.coffee
+++ test/tests.coffee
@@ -85,8 +85,8 @@
         require '../testFixtures/testWithExcludes/b/bar.coffee'

         expect(global[COVERAGE_VAR], "Code should have been instrumented").to.exist
-        expect(global[COVERAGE_VAR]['a/foo.coffee'], "Should instrument a/foo.coffee").to.exist
-        expect(global[COVERAGE_VAR]['b/bar.coffee'], "Should not instrument b/bar.coffee").to.not.exist
+        expect(global[COVERAGE_VAR][pn 'a/foo.coffee'], "Should instrument a/foo.coffee").to.exist
+        expect(global[COVERAGE_VAR][pn 'b/bar.coffee'], "Should not instrument b/bar.coffee").to.not.exist

     it "should handle nested recursion correctly", ->
         # From https://github.com/benbria/coffee-coverage/pull/37


parse body

parse body

--- app.coffee
+++ app.coffee
@@ -6,7 +6,9 @@

 app.set 'port', (process.env.PORT || 5000)
 # app.use express.static __dirname + '/public'
-app.use bodyParser.json()
+# app.use bodyParser.json()
+app.use bodyParser.urlencoded
+  extended: false

 app.get '/', (req, res) ->
   twiml = new twilio.TwimlResponse()


remove (, )

remove (, )

--- app.coffee
+++ app.coffee
@@ -64,14 +64,13 @@
   for uri of options.services
     app.post uri, (req, res) ->
       svc = options.services[req.path]
-      req.on('data', svc.transport.receiver (transportWithData) ->
+      req.on 'data', svc.transport.receiver (transportWithData) ->
         input = new svc.protocol(transportWithData)
-        output = new svc.protocol(new svc.transport(undefined, (buf) ->
+        output = new svc.protocol new svc.transport undefined, (buf) ->
           res.writeHead 200
           res.end buf
-        ))
         svc.processor.process(input, output)
-      )
+

 initThriftService app, ThriftServerOptions


remove app.configure

remove app.configure

--- app.coffee
+++ app.coffee
@@ -16,16 +16,7 @@
 Metrics.event_loop?.monitor(logger)
 Metrics.memory.monitor(logger)

-app.configure ->
-	app.use Metrics.http.monitor(logger)
-
-app.configure 'development', ->
-	console.log "Development Enviroment"
-	app.use express.errorHandler({ dumpExceptions: true, showStack: true })
-
-app.configure 'production', ->
-	console.log "Production Enviroment"
-	app.use express.errorHandler()
+app.use Metrics.http.monitor(logger)

 Metrics.inc "startup"


just triggering codeship deployment pipline

--- app.coffee
+++ app.coffee
@@ -162,6 +162,7 @@

     res.render 'rss', posts: posts

+# random comment.
 app.listen app.get('port'), ->
   console.log 'Express server listening on port ' + app.get 'port'


More tests

More tests

--- test/utils.coffee
+++ test/utils.coffee
@@ -205,8 +205,30 @@
   describe '.addDependencies', ->
     Given -> @child_process.exec = sinon.stub()
     context 'regular deps', ->
-      Given -> @child_process.exec.withArgs('npm install foo bar baz --save --save-exact', sinon.match.func).callsArgWith(1, null, 'info', null)
+      Given -> @child_process.exec.withArgs('npm install foo bar baz --save --save-exact', sinon.match.func).callsArgWith(1, null, 'fake npm output', null)
       Given -> @deps = ['foo', 'bar', 'baz']
       Given -> @done = sinon.spy()
       When -> @subject.addDependencies @deps, @done
       Then -> expect(@done).to.have.been.called
+
+    context 'dev deps', ->
+      Given -> @child_process.exec.withArgs('npm install foo bar baz --save-dev --save-exact', sinon.match.func).callsArgWith(1, null, 'fake npm output', null)
+      Given -> @deps = ['foo', 'bar', 'baz']
+      Given -> @done = sinon.spy()
+      When -> @subject.addDependencies @deps, true, @done
+      Then -> expect(@done).to.have.been.called
+
+    context 'error', ->
+      Given -> @child_process.exec.withArgs('npm install foo bar baz --save-dev --save-exact', sinon.match.func).callsArgWith(1, 'error', null, null)
+      Given -> @deps = ['foo', 'bar', 'baz']
+      Given -> @done = sinon.spy()
+      When -> @subject.addDependencies @deps, true, @done
+      Then -> expect(@done).to.have.been.called
+
+    context 'stderr', ->
+      Given -> @child_process.exec.withArgs('npm install foo bar baz --save-dev --save-exact', sinon.match.func).callsArgWith(1, null, null, 'fake stderr')
+      Given -> @deps = ['foo', 'bar', 'baz']
+      Given -> @done = sinon.spy()
+      When -> @subject.addDependencies @deps, true, @done
+      Then -> expect(@done).to.have.been.called
+


only show first name if asking for 'login'

--- models/person.coffee
+++ models/person.coffee
@@ -74,7 +74,7 @@
 ROLES.forEach (t) ->
   PersonSchema.virtual(t).get -> @role == t
 PersonSchema.virtual('login').get ->
-  @github?.login or @twit?.screenName or @name
+  @github?.login or @twit?.screenName or @name.split(' ')[0]
 PersonSchema.virtual('githubLogin').get -> @github?.login
 # twitterScreenName isn't here because you can edit it


updated doctags-regex incorporating kmdavis' proposal

--- lib/utils.coffee
+++ lib/utils.coffee
@@ -284,7 +284,7 @@
     pygmentize.stdin.end()

   parseDocTags: (segments, project, callback) ->
-    TAG_REGEX = /(^|\s)@(\w+)(?:\s+(.*))?/
+    TAG_REGEX = /(?:^|\s)@(\w+)(?:\s+(.*))?/
     TAG_VALUE_REGEX = /^(?:"(.*)"|'(.*)'|\{(.*)\}|(.*))$/

     try
@@ -300,8 +300,8 @@
         for line in segment.comments when line?
           if (match = line.match TAG_REGEX)?
             currTag = {
-              name: match[2]
-              value: match[3] || ''
+              name: match[1]
+              value: match[2] || ''
             }
             tags.push currTag
           else


Support additional quote characters

Support additional quote characters

Possible cause of issue #5 where the bot doesn't respond.  Hubot for Slack, combined with the Slack Desktop client will not respond as the Slack client auto-corrects quotation marks to their curled versions.

--- src/trello.coffee
+++ src/trello.coffee
@@ -75,7 +75,7 @@
       for list in data
         lists[list.name.toLowerCase()] = list

-  robot.respond /trello new ["'](.+)["']\s(.*)/i, (msg) ->
+  robot.respond /trello new ["“'‘](.+)["”'’]\s(.*)/i, (msg) ->
     ensureConfig msg.send
     card_name = msg.match[2]
     list_name = msg.match[1]
@@ -91,10 +91,10 @@

     createCard msg, list_name, card_name

-  robot.respond /trello list ["'](.+)["']/i, (msg) ->
+  robot.respond /trello list ["“'‘](.+)["”'’]/i, (msg) ->
     showCards msg, msg.match[1]

-  robot.respond /trello move (\w+) ["'](.+)["']/i, (msg) ->
+  robot.respond /trello move (\w+) ["“'‘](.+)["”'’]/i, (msg) ->
     moveCard msg, msg.match[1], msg.match[2]

   robot.respond /trello list lists/i, (msg) ->


Created Search functionality

Created Search functionality

--- src/trello.coffee
+++ src/trello.coffee
@@ -16,6 +16,8 @@
 #   hubot trello add member <shortlink> "<first name of member>"
 #   hubot trello add comment <shortlink> "<comment>"
 #   hubot trello description <shortlink> "<discription>"
+#   hubot trello search "<criteria>"
+#   hubot trello get <name>'s cards
 #
 #
 # Author:
@@ -82,13 +84,20 @@
      id = members[member_name.toLowerCase()].username
   msg.send "Unable to find person named: #{user_name}" unless id?
   if id?
-    trello.get "/1/search", {query: "@"+id, idBoards: board.id, modelTypes:cards, card_fields: name,shortLink,url}, (err, data) ->
+    trello.get "/1/search", {query: "@"+id, idBoards: board.id, modelTypes: "cards", card_fields: "name,shortLink,url"}, (err, data) ->
       msg.reply "So sorry, I got an error and cannot give you that information" if err
       msg.reply "I got the following cards for you" unless err
-      msg.send board.id
-      msg.send id
       for cards in data.cards
         msg.send " * " + cards.name+ " | " + cards.url
+
+search = (msg, search) ->
+  if (search?)
+    msg.reply "I'm searching for results now"
+    trello.get "/1/search", {query: search, idBoards: board.id, modelTypes:"cards", card_fields: "name,shortLink,url"}, (err, data) ->
+      msg.reply "Sorry, I was unable to search at this time. Please try again later." if err
+      msg.reply "The following cards match your criteria" unless err
+      for cards in data.cards
+         msg.send " * " + cards.name + " | " + cards.url

 moveCard = (msg, card_id, list_name) ->
@@ -160,11 +169,14 @@
     msg.reply "Here are all the lists on your board."
     Object.keys(lists).forEach (key) ->
       msg.send " * " + key
+
+  robot.respond /trello search ["“'‘]((.+|\n)+)["”'’]/i, (msg) ->
+     search msg, msg.match[1]

   robot.respond /trello list members/i, (msg) ->
     msg.reply "Here are all the members of the board"
     Object.keys(members).forEach (member)->
-  msg.send " * " + member
+    msg.send " * " + member

   robot.respond /trello list member id (\w+)/i, (msg) ->
     msg.reply "The member id for "+ msg.match[1]+" is: "
@@ -181,6 +193,8 @@
     msg.send " *  trello move <card.shortlink> \"<ListName>\""
     msg.send " *  trello list lists"
     msg.send " *  trello list member id <name>"
+    msg.send " *  trello get <first name>'s cards"
     msg.send " *  trello add member <card.shortLink> <Name>"
     msg.send " *  trello comment <card.shortLink> <Comment>"
     msg.send " *  trello description <card.shortLink> <Description>"
+    msg.send " *  trello search <criteria>"


allows window to be closed on a startup error

instead of closing to tray

--- src/ui/app.coffee
+++ src/ui/app.coffee
@@ -79,6 +79,19 @@
              remote.getGlobal('windowHideWhileCred') != true
         mainWindow.show()

+    #
+    window.addEventListener 'unload', (ev) ->
+        if process.platform == 'darwin' && window?
+            if window.isFullScreen()
+                window.setFullScreen false
+            if not remote.getGlobal('forceClose')
+                ev.preventDefault()
+                window?.hide()
+                return
+
+        window = null
+        action 'quit'
+
 #
 #
 # Get information on exceptions in main process
@@ -149,19 +162,6 @@
 window.addEventListener 'offline', -> action 'wonline', false

 #
-window.addEventListener 'unload', (ev) ->
-    if process.platform == 'darwin' && window?
-        if window.isFullScreen()
-            window.setFullScreen false
-        if not remote.getGlobal('forceClose')
-            ev.preventDefault()
-            window?.hide()
-            return
-
-    window = null
-    action 'quit'
-
-#
 #
 # Catch unresponsive events
 remote.getCurrentWindow().on 'unresponsive', (error) ->


Made required authentication configurable

--- cs_app/auth.coffee
+++ cs_app/auth.coffee
@@ -13,8 +13,12 @@

   # middleware to authenticate a user.
   # if a user defined in settings.userFile is found everything is okay, otherwise the alternative
-  # authentication strategy will be used.
+  # authentication strategy will be used if enabled.
   @authUser = (req, res, next) ->
+    if not settings.authRequired
+      req.user = 'anonymous'
+      return next()
+
     users = functions.getUsers()

     if req.body? and req.body.user? and req.body.pass?


fix #901 you are in read mode toast should not be sticky

--- app/assets/javascripts/oxalis/model/helpers/restriction_handler.coffee
+++ app/assets/javascripts/oxalis/model/helpers/restriction_handler.coffee
@@ -22,6 +22,6 @@

     else
       if not @issuedUpdateError
-        Toast.error(@UPDATE_ERROR, true)
+        Toast.error(@UPDATE_ERROR)
         @issuedUpdateError = true
       return true


Add better shutdown checking in SIGTERM

Add better shutdown checking in SIGTERM

--- lifeguard.coffee
+++ lifeguard.coffee
@@ -16,6 +16,21 @@
       process.exit()

     @dir = path.resolve(@dir)
+
+    # create a debounced function for calling restart, so that we don't
+    # trigger multiple times in a row.  This would just be _.debounce,
+    # but bringing underscore in for one thing seemed silly
+
+    @debounceRestart = do =>
+      _timeout = null
+      _ts = 1000
+
+      =>
+        clearTimeout(_timeout)
+        _timeout = setTimeout =>
+          _timeout = null
+          @_restartInstance()
+        , _ts

     # -- Startup -- #

@@ -31,22 +46,32 @@
     # if we get a HUP, pass it through to our instance
     process.on "SIGHUP", => process.kill @instance.child.pid, "SIGHUP"

-    process.on "SIGTERM", =>
-      # need to shut down instance, but do it gracefully
-      process.kill @instance.child.pid, "SIGINT"
-
+    process.on "SIGTERM", => @_shutDown()
+
+    process.on "uncaughtException", => @_shutDown()
+
     # start our new process, if the app directory exists.  if it doesn't, just
     # watch and wait
     @_watchForDir path.resolve(@dir,"tmp/restart.txt"), => @_startUp()

   #----------

+  _shutDown: ->
+    @_notify "Lifeguard got TERM signal. Shutting down."
+
+    # need to shut down instance, but do it gracefully
+    @instance.forceStop = true
+    process.kill @instance.child.pid, "SIGINT"
+
+    @instance.on "exit", =>
+        @_notify "Child process has exited."
+        process.exit()
+
   # If our entire path doesn't yet exist, walk backward until we find the
   # longest part that does. watch that directory for the next step in the
   # tree to appear

   _watchForDir: (dir,cb) ->
-    console.log "watchForDir trying #{dir}"
     # loop our way up until we find something that exists
     lFunc = (d,lcb) =>
       # test this
@@ -61,7 +86,6 @@
             lFunc(d,lcb)

     lFunc dir, (existing) =>
-      console.log "_watchForDir pass came out with #{existing}"
       if existing == dir
         console.log "Starting up!"
         cb?()
@@ -82,7 +106,7 @@
     # set up a watcher on tmp/restart.txt
     @r_watcher = fs.watch path.resolve(@dir,"tmp/restart.txt"), (type,filename) =>
       # only restart on a touch, not on a deletion (such as when current/ is unlinked)
-      @_restartInstance() if fs.existsSync("#{@dir}/tmp/restart.txt")
+      @debounceRestart() if fs.existsSync("#{@dir}/tmp/restart.txt")

     # watch for our directory to change out from under us
     d_base = path.basename(@dir)
@@ -94,7 +118,7 @@

         @_watchForDir path.resolve(@dir,"tmp/restart.txt"), => @_startUp()

-    @_restartInstance()
+    @debounceRestart()

   #----------


If no files are selected, submit form as normal.

--- app/assets/javascripts/anaconda_uploader.js.coffee
+++ app/assets/javascripts/anaconda_uploader.js.coffee
@@ -31,7 +31,7 @@

   form_submit_handler: (e) ->
     self = e.data.self
-    return if self.upload_automatically
+    return true if self.upload_automatically || self.all_uploads_are_complete
     e.preventDefault()
     $(this).off( 'submit', self.form_submit_handler )

@@ -42,7 +42,15 @@
     for upload_field, i in @anaconda_upload_fields
       upload_field.reset()
     @anaconda_upload_fields = []
-
+
+  all_uploads_are_complete: ->
+    all_completed = true
+    for upload_field, i in @anaconda_upload_fields
+      if upload_field.upload_in_progress
+        all_completed = false
+        break
+    return all_completed
+
   upload_completed: ->
     all_completed = true
     for upload_field, i in @anaconda_upload_fields


Fixed searchfield gets hidden on mouseup in Chrome since v.73

--- app/assets/javascripts/internal/masthead.js.coffee
+++ app/assets/javascripts/internal/masthead.js.coffee
@@ -78,8 +78,8 @@
     $mastheadSearch.find("div").addClass("input-append input-prepend")

     # Close on click outside the searchbox
-    $(document).on 'click.searchForm', (event) ->
-      hideSearch() unless $(event.target).closest("#masthead-search, #nav-search-trigger").length
+    $(document).one 'mousedown', (event) ->
+        hideSearch() unless $(event.target).closest("#masthead-search, #nav-search-trigger").length

   # Bind escape key to hide form
   $mastheadSearch.focusin ->


Now the show.jst.hamlc page updates it's header when the model is changed.

--- app/assets/javascripts/views/clouds/show.js.coffee
+++ app/assets/javascripts/views/clouds/show.js.coffee
@@ -23,11 +23,14 @@
       @refreshGfx()

   refreshGfx: ->
-    # ...
+    @.$('.cloud-head img').attr('src',@model.get('avatar').normal)
+    @.$('.cloud-head > h2').text(@model.get('name'))
+    @.$('.cloud-head > p').text(@model.get('description'))

   show: ->
     $('.view-container').removeClass('active')
     $(@el).addClass('active')
+
     if @.$('.chat-container').length == 0
       @chat_view = new Cloudsdale.Views.CloudsChat(model: @model)
       @.$('.float-container').html(@chat_view.el)


Don't log the task result.

Don't log the task result.

--- src/worker.coffee
+++ src/worker.coffee
@@ -28,7 +28,7 @@
         console.log "Task failed: %s (%s)", id, error
         "failed"
       else
-        console.log "Task completed: %s (%s)", id, data
+        console.log "Task completed: %s", id
         "completed"
       payload = JSON.stringify id: id, response: response, data: data
       @socket.send payload


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/color.coffee
+++ test/color.coffee
@@ -15,7 +15,7 @@
   color = Color('rgb(4, 66, 134, 0.45)')
   equals color.r(), 4, 'red channel should equal input red value'
   equals color.g(), 66, 'green channel should equal input green value'
-  equals color.g(), 134, 'blue channel should equal input blue value'
+  equals color.b(), 134, 'blue channel should equal input blue value'
   equals color.a(), 0.45, 'alpha channel should equal input alpha value'

 ###


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/color.coffee
+++ test/color.coffee
@@ -1,5 +1,6 @@
-test "parseColor", ->
+test "Color default color is black", ->
   color = Color()
+  color.
   equals((5).abs(), 5, "(5).abs() equals 5")
   equals((4.2).abs(), 4.2, "(4.2).abs() equals 4.2")
   equals((-1.2).abs(), 1.2, "(-1.2).abs() equals 1.2")


removed me.coffe

removed me.coffe

--- packages/rocketchat-slashcommands-me/me.coffee
+++ packages/rocketchat-slashcommands-me/me.coffee
@@ -1,16 +0,0 @@
-###
-# Me is a named function that will replace /me commands
-# @param {Object} message - The message object
-###
-
-class Me
-	constructor: (command, params, item) ->
-		if(command == "me")
-			if _.trim params
-				msg = item
-				msg.msg = '_' + params + '_'
-				Meteor.call 'sendMessage', msg
-
-RocketChat.slashCommands.add 'me', Me,
-	description: 'Displays_action_text'
-	params: 'your_message'


Enable notification reply when supported

--- packages/rocketchat-ui/lib/notification.coffee
+++ packages/rocketchat-ui/lib/notification.coffee
@@ -19,12 +19,19 @@
 					body: _.stripTags(message.msg)
 					tag: notification.payload._id,
 					silent: true
+					canReply: true

 				notificationDuration = (notification.duration - 0) or (Meteor.user()?.settings?.preferences?.desktopNotificationDuration - 0) or RocketChat.settings.get('Desktop_Notifications_Duration')
 				if notificationDuration > 0
 					setTimeout ( -> n.close() ), notificationDuration * 1000

 				if notification.payload?.rid?
+					n.addEventListener 'reply', ({response}) ->
+						Meteor.call 'sendMessage',
+							_id: Random.id()
+							rid: notification.payload.rid
+							msg: response
+
 					n.onclick = ->
 						this.close()
 						window.focus()


Delete api token

Delete api token

--- views/env.coffee
+++ views/env.coffee
@@ -207,6 +207,7 @@
     window.dispatchEvent event
   locked = false
 proxy.addListener 'game.on.response', (method, path, body, postBody) ->
+  delete postBody.api_token if postBody?.api_token?
   responses.push [method, path, body, postBody]
   resolveResponses() if !locked
 proxy.addListener 'game.start', ->


Update test to verify fix for issue #100

--- test/tests.coffee
+++ test/tests.coffee
@@ -70,25 +70,6 @@
     ["#{testPath}/*.coffee"],
     css: "#{resourcesPath}/pagelet.css"

-# Issue 100: Verify that URL references work across doc and code sections.
-test "url references", ->
-  exec "mkdir -p #{dataPath}", ->
-    sourceFile = "#{testPath}/data/_urlref.coffee"
-    fs.writeFileSync sourceFile, """
-    # Look at this link to [Google][]!
-    console.log 'This must be Thursday.'
-    # And this link to [Google][] as well.
-    console.log 'I never could get the hang of Thursdays.'
-    # [google]: http://www.google.com
-    """
-    outPath = "#{testPath}/data/_urlreferences"
-    outFile = "#{outPath}/_urlref.html"
-    exec "rm -rf #{outPath}", ->
-      Docco.document [sourceFile], output: outPath, ->
-        contents = fs.readFileSync(outFile).toString()
-        count = contents.match ///Google///g
-        eq count.length, 2, "find expected (2) resolved url references"
-
 #### Docco Comment Parser

 # Verify we can parse expected comments from each supported language.
@@ -152,3 +133,23 @@
     options.blocks = false
     testNextLanguage languageKeys.slice()

+# **URL references should resolve across sections**
+#
+# Resolves [Issue 100](https://github.com/jashkenas/docco/issues/100)
+test "url references", ->
+  exec "mkdir -p #{dataPath}", ->
+    sourceFile = "#{dataPath}/_urlref.coffee"
+    fs.writeFileSync sourceFile, [
+      "# Look at this link to [Google][]!",
+      "console.log 'This must be Thursday.'",
+      "# And this link to [Google][] as well.",
+      "console.log 'I never could get the hang of Thursdays.'",
+      "# [google]: http://www.google.com"
+    ].join('\n')
+    outPath = path.join dataPath, "_urlreferences"
+    outFile = "#{outPath}/_urlref.html"
+    exec "rm -rf #{outPath}", ->
+      Docco.document [sourceFile], output: outPath, ->
+        contents = fs.readFileSync(outFile).toString()
+        count = contents.match ///Google///g
+        eq count.length, 2, "find expected (2) resolved url references"


Failing test

Failing test

--- test/authentication_test.coffee
+++ test/authentication_test.coffee
@@ -89,7 +89,7 @@
     before (done) ->
       brains.get "/auth/script", (req, res) ->
         if auth = req.headers.authorization
-	  res.send """
+          res.send """
           <html>
             <head>
               <title>Zero</title>
@@ -101,7 +101,7 @@
         else
           res.send "No Credentials", 401

-      brains.get "auth/script.js", (req, res) ->
+      brains.get "/auth/script.js", (req, res) ->
         if auth = req.headers.authorization
           res.send "document.title = document.title + 'One'"
         else


Fixed random generator

Fixed random generator

--- bot.coffee
+++ bot.coffee
@@ -9,7 +9,7 @@

 rand = (n) ->
-  (Math.random(Math.random()*100000 + new Date().getTime()) % n)
+  Math.floor(Math.random()*100000 + new Date().getTime()) % (n+1)

 randomMove = ->
   "#{rand(7)}#{rand(7)}"


fixed issue in bulkhead

fixed issue in bulkhead

--- lib/bulkhead.coffee
+++ lib/bulkhead.coffee
@@ -53,7 +53,7 @@
   # console.log '------ GET FILE', filename
   o = get_file_ref(filename)
   res = {is_packaged: o?}
-  res.path = o.path if o.path?
+  res.path = o?.path if o?.path?
   res.file = o.tree[o.path] if o?.tree?[o.path]?
   # console.log '------ GET FILE', res
   res


add tests for queue transform if error midstream

--- test/queue.coffee
+++ test/queue.coffee
@@ -66,3 +66,25 @@
       assert.ifError err
       assert.deepEqual result, input
       done()
+
+  it 'successfully ends stream if errors midstream in the queue', (done) ->
+    input  = ({a: i} for i in [0...100])
+    fn = (chunk, cb) ->
+      if chunk.a in [3,6,9,12]
+        return cb 'some error'
+      cb()
+    new Understream(input).queue(fn).run (err) ->
+      assert.equal err, 'some error'
+      done()
+
+  it 'successfully ends stream with first error if multiple errors', (done) ->
+    input  = ({a: i} for i in [0...100])
+    fn = (chunk, cb) ->
+      if chunk.a is 3
+        return cb 'first error'
+      if chunk.a in [6,9,12,15]
+        return cb 'some error'
+      cb()
+    new Understream(input).queue(fn).run (err) ->
+      assert.equal err, 'first error'
+      done()


'i will test the compiler on something other than arm' x 50

--- lib/compiler.coffee
+++ lib/compiler.coffee
@@ -133,7 +133,7 @@

         createCall: (fromFunction, callee, argv, callname) -> ir.createCall callee, argv, callname
         createInvoke: (fromFunction, callee, argv, normal_block, exc_block, callname) -> ir.createInvoke callee, argv, normal_block, exc_block, callname
-        createRet: (fromFunction, value) -> ir.createRet val
+        createRet: (fromFunction, value) -> ir.createRet value
         createExternalFunction: (inModule, name, ret_type, param_types) -> inModule.getOrInsertExternalFunction name, ret_type, param_types
         createFunction: (inModule, name, ret_type, param_types) -> inModule.getOrInsertFunction name, ret_type, param_types
         createFunctionType: (ret_type, param_types) -> llvm.FunctionType.get ret_type, param_types


ensure that class-list.cson exists before we try to delete it

--- lib/database.coffee
+++ lib/database.coffee
@@ -14,7 +14,8 @@
     rimraf.sync(@path() + "pupils/", (e) ->
       alert("Deletion Error: " + e)
     )
-    fs.unlinkSync(@path() + "class-list.cson")
+    if(fs.existsSync(@path() + "class-list.cson"))
+      fs.unlinkSync(@path() + "class-list.cson")

   loadCache: ->
     @cache = Cache


#23 add tests for  with null (see SERVER-23664) https://jira.mongodb.org/browse/SERVER-23664

--- test/suite.coffee
+++ test/suite.coffee
@@ -107,6 +107,13 @@
     result = _query a, likes: {$between: [null, 5]}
     assert.equal result.length, 0

+  it "$between errors with not enough args", ->
+    a = create()
+    assert.throws ->
+      result = _query a, likes: {$between: []}
+    assert.throws ->
+      result = _query a, likes: {$between: [5]}
+
   it "$betweene operator is inclusive", ->
     a = create()
     result = _query a, likes: {$betweene: [1,2]}
@@ -123,6 +130,21 @@
     result = _query a, likes: {$mod: [3,0]}
     assert.equal result.length, 1
     assert.equal result[0].title, "Home"
+
+
+  it "$mod operator with null", ->
+    a = create()
+    result = _query a, likes: {$mod: [null, 5]}
+    assert.equal result.length, 0
+    result = _query a, likes: {$mod: [3, null]}
+    assert.equal result.length, 0
+
+  it "$mod errors with not enough args", ->
+    a = create()
+    assert.throws ->
+      result = _query a, likes: {$mod: []}
+    assert.throws ->
+      result = _query a, likes: {$mod: [5]}

   it "$in operator", ->
     a = create()


Add new test data to test suit

Add new test data to test suit

--- test/suite.coffee
+++ test/suite.coffee
@@ -1,32 +1,27 @@
 "use strict"

 request = require 'supertest'
-assert = require 'assert'
+assert  = require 'assert'
+data    = require './util/data.coffee'

-exports.app = app = require './../coffee/server.coffee'
-exports.data = data = require('./util/data-gen.coffee')(100)
-exports.mongo = mongo = null
-
-# @TODO ok, so we had to hack this
-data[50].status = "Offentlig"
-data[51].status = "Offentlig"
-data[52].status = "Offentlig"
-data[53].status = "Offentlig"
-data[53].status = "Offentlig"
-
-exports.cache = redis = null
+exports.app   = app = require './../coffee/server.coffee'

 before (done) -> app.once 'ready', done
 beforeEach (done) ->
-  mongo = app.get 'db'
   cache = app.get 'cache'
+  redis = cache.redis
+  mongo = cache.mongo

-  cache.flushall()
-  mongo.collection('turer').drop (err) ->
-    #throw err if err
-    mongo.collection('turer').insert data, {safe: true}, (err) ->
-      throw err if err
-      done()
+  redis.flushall()
+  mongo.dropCollection 'turer'
+  mongo.dropCollection 'steder'
+
+  cnt = 2
+  for type in data.getTypes()
+    mongo.collection(type).insert data.get(type, true), {safe: true, w: 1}, (err, msg) ->
+      assert.ifError(err)
+      done() if --cnt is 0
+

 describe 'ntb.api', ->
   describe '/', ->


Bug induced by CS for loops

Bug induced by CS for loops

--- test/spec/Evaluator-spec.coffee
+++ test/spec/Evaluator-spec.coffee
@@ -28,7 +28,12 @@

 	for title, its of window.snippets
-		describe title, ->
-			for text, spec of its
-				it text, ->
-					(expect jinterEv spec).toEqual (jsEv spec)+		do (title, its) ->
+			describe title, ->
+				for text, spec of its
+					do (text, spec) ->
+						it text, ->
+							(expect jinterEv spec).toEqual (jsEv spec)
+							return
+				return
+	return


Style - formatting

Style - formatting

--- test/unit/file-list.spec.coffee
+++ test/unit/file-list.spec.coffee
@@ -10,15 +10,15 @@

   mockGlob = mocks.glob.create
     '/some/*.js': ['/some/a.js', '/some/b.js']
-    '*.txt': ['/c.txt', '/a.txt', '/b.txt']
-    '*.js': ['/folder', '/folder/x.js']
-    '/a.*': ['/a.txt']
+    '*.txt':      ['/c.txt', '/a.txt', '/b.txt']
+    '*.js':       ['/folder', '/folder/x.js']
+    '/a.*':       ['/a.txt']

   mockFs = mocks.fs.create
     some:
       '0.js': mocks.fs.file '2012-04-04'
-      'a.js':   mocks.fs.file '2012-04-04'
-      'b.js':   mocks.fs.file '2012-05-05'
+      'a.js': mocks.fs.file '2012-04-04'
+      'b.js': mocks.fs.file '2012-05-05'
     folder:
       'x.js': mocks.fs.file 0
     'a.txt': mocks.fs.file 0


Add unit test case for utils.getToken

Add unit test case for utils.getToken

--- test/utils.coffee
+++ test/utils.coffee
@@ -221,3 +221,20 @@
       assert.equal 'ntrfv', utils.nt2space 'ntrfv'
       done()

+  describe '#getToken', ->
+    it "优先获取头信息里的 x-auth-token", (done) ->
+      req =
+        headers:
+          "x-auth-token": "Hi, I'm token"
+        params:
+          access_token: "access_token"
+          accessToken: "accessToken"
+
+      assert.equal "Hi, I'm token", utils.getToken(req)
+      req.headers = {}
+      assert.equal "access_token", utils.getToken(req)
+      req.params.access_token = null
+      assert.equal "accessToken", utils.getToken(req)
+
+      done()
+


:memo: for what happens

:memo: for what happens

--- lib/operator.coffee
+++ lib/operator.coffee
@@ -306,7 +306,7 @@
       @vimState.operationStack.run(transformer.name, {target: @target.constructor.name})

   execute: ->
-    # NEVER be executed
+    # NEVER be executed since operationStack is replaced with selected transformer

 class TransformWordBySelectList extends TransformStringBySelectList
   @extend()


string replaced by regex

string replaced by regex

--- lib/provider.coffee
+++ lib/provider.coffee
@@ -144,7 +144,7 @@

             formattedParams = matches[4].split(',').map(
                 (item) ->
-                    item.split(' ').filter(
+                    item.split(/\s/).filter(
                         (item) ->
                             item != ''
                     ).join(' ')


Support for ~~strikethrough~~. Fix #87

Support for ~~strikethrough~~. Fix #87

--- lib/sanitize.coffee
+++ lib/sanitize.coffee
@@ -15,3 +15,4 @@
 sanitizeHtml.defaults.allowedTags.push 'source'
 sanitizeHtml.defaults.allowedAttributes.source = ['src']
 sanitizeHtml.defaults.selfClosing.push 'source'
+sanitizeHtml.defaults.allowedTags.push 'del'  ## for Markdown ~~strikethrough~~


Use unless instead of if not

Use unless instead of if not

--- lib/snippets.coffee
+++ lib/snippets.coffee
@@ -49,8 +49,8 @@

   loadTextMateSnippets: (bundlePath, done) ->
     snippetsDirPath = path.join(bundlePath, 'Snippets')
-    if not fs.isDirectorySync(snippetsDirPath)
-      snippetsDirPath = path.join(bundlePath, "snippets")
+    unless fs.isDirectorySync(snippetsDirPath)
+      snippetsDirPath = path.join(bundlePath, 'snippets')

     return done() unless fs.isDirectorySync(snippetsDirPath)


add missing space when debug logging heartbeat command

--- lib/wakatime.coffee
+++ lib/wakatime.coffee
@@ -403,7 +403,7 @@
             args.push(path.basename(realPath))
             break

-      log.debug python + args.join(' ')
+      log.debug python + ' ' + args.join(' ')

       proc = execFile(python, args, (error, stdout, stderr) ->
         if error?


Fixed #181 no Actions cell will be rendered when actions are empty

--- wzk/ui/grid/RowBuilder.coffee
+++ wzk/ui/grid/RowBuilder.coffee
@@ -23,9 +23,7 @@

     row.addClassName cls for cls in model['_class_names']

-    if goog.array.isEmpty model['_actions']
-      row.addCell ''
-    else
+    unless goog.array.isEmpty model['_actions']
       @buildActionsCell row, model

     row


More offset...

More offset...

--- modules/player/commands.coffee
+++ modules/player/commands.coffee
@@ -352,7 +352,7 @@

     @registerCommand 'screenshot', { aliases: ['ss'] }, ({ m, l, player })=>
       return m.reply l.generic.cooldown if @cooldown[m.guild.id] > Date.now()
-      @cooldown[m.guild.id] = Date.now() + 10000
+      @cooldown[m.guild.id] = Date.now() + 5000

       { nowPlaying } = player.queue
       return m.reply l.player.notPlaying unless nowPlaying
@@ -360,7 +360,7 @@

       try
         Core.util.sendTyping(m.channel)
-        time = if nowPlaying.duration then nowPlaying.time + 1 else 0
+        time = if nowPlaying.duration then nowPlaying.time + 2 else 0
         screenshot = await @util.getScreenshot nowPlaying.videoPath, time
         m.channel.send files: [{
           name: "focabot-#{nowPlaying.uid}-#{time}.jpg"


Render unit selector on collection reset

--- app/assets/javascripts/langtrainer_frontend_backbone/views/unit_selector.js.coffee
+++ app/assets/javascripts/langtrainer_frontend_backbone/views/unit_selector.js.coffee
@@ -3,7 +3,7 @@
   id: 'unit-selector'

   initialize: ->
-    @listenTo @collection, 'change', @render
+    @listenTo @collection, 'reset', @render

   render: ->
     that = @


Client: Activity: Added getChangedFields method to create changed notification settings fields object

--- client/activity/lib/components/publicchannelnotificationsettingsmodal/index.coffee
+++ client/activity/lib/components/publicchannelnotificationsettingsmodal/index.coffee
@@ -31,6 +31,25 @@
     radio = webNotifications.querySelector "input[value=#{value}]"

     radio.focus()
+
+
+  getChangedFields: ->
+
+    channelSettings = @state.channelNotificationSettings.toJS()
+    initialSettings = if @initialSettings then @initialSettings.toJS() else channelSettings
+    changedKeys     = Object.keys(initialSettings).filter (key) -> initialSettings[key] isnt channelSettings[key]
+    changedFields   = channelSettings
+
+    if not channelSettings._newlyCreated
+
+      changedFields = {}
+
+      i = 0
+      while i < changedKeys.length
+        changedFields[changedKeys[i]] = channelSettings[changedKeys[i]]
+        i++
+
+    return changedFields

   onSave: (event) ->


reorganizing color tests into module

reorganizing color tests into module

--- test/color.coffee
+++ test/color.coffee
@@ -147,3 +147,10 @@
   black = Color(0, 0, 0)
   equals white.toHex(), "#ffffff"
   equals black.toHex(), "#000000"
+
+test "should recognize an object with a channels array as a Color", ->
+  red = Color(255, 0, 0)
+  testColor = Color(red)
+  equals testColor.r(), 255
+  equals testColor.g(), 0
+  equals testColor.b(), 0


Fix incorrect mapping of images to views.

--- client/app.coffee
+++ client/app.coffee
@@ -57,10 +57,10 @@
   timestamp = Template.currentData().imageTimestamp
   switch Session.get 'view'
     when 'view-screen'
-      file = 'screen'
+      file = 'presentation'
     when 'view-camera'
       file = 'presenter'
     when 'view-galicaster'
-      file = 'presentation'
+      file = 'screen'
   url = "/image/#{roomId}/#{file}"
   if timestamp then "#{url}?#{timestamp}" else url


add tests for write_config

add tests for write_config

--- test/index.coffee
+++ test/index.coffee
@@ -1,4 +1,7 @@
-path = require('path')
+path   = require 'path'
+fs     = require 'fs'
+yaml   = require 'js-yaml'
+nodefn = require 'when/node'

 describe 'api', ->

@@ -56,8 +59,24 @@
         .should.be.fulfilled

   describe 'write_config', ->
-    it 'should write a shipfile with the config info to the project root'
-    it 'should error if instance has not been configured'
+
+    it 'should write a shipfile with the config info to the project root', ->
+      project = new Ship(root: __dirname, deployer: 's3')
+      project.configure(access_key: 'foo', secret_key: 'bar')
+      shipfile = path.join(__dirname, 'ship.conf')
+
+      project.write_config()
+        .then(nodefn.lift(fs.readFile, shipfile, 'utf8'))
+        .then(yaml.safeLoad)
+        .tap (res) ->
+          res.s3.access_key.should.equal('foo')
+          res.s3.secret_key.should.equal('bar')
+        .then(-> fs.unlinkSync(shipfile))
+        .should.be.fulfilled
+
+    it 'should error if instance has not been configured', ->
+      project = new Ship(root: __dirname, deployer: 's3')
+      (-> project.write_config()).should.throw('deployer has not yet been configured')

   describe 'deploy', ->
     it 'should load in root/shipfile.conf as config if present'