1// this file handles outputting usage instructions,
2// failures, etc. keeps logging in one place.
3var cliui = require('cliui'),
4  decamelize = require('decamelize'),
5  wsize = require('window-size')
6
7module.exports = function (yargs) {
8  var self = {}
9
10  // methods for ouputting/building failure message.
11  var fails = []
12  self.failFn = function (f) {
13    fails.push(f)
14  }
15
16  var failMessage = null
17  var showHelpOnFail = true
18  self.showHelpOnFail = function (enabled, message) {
19    if (typeof enabled === 'string') {
20      message = enabled
21      enabled = true
22    } else if (typeof enabled === 'undefined') {
23      enabled = true
24    }
25    failMessage = message
26    showHelpOnFail = enabled
27    return self
28  }
29
30  self.fail = function (msg) {
31    if (fails.length) {
32      fails.forEach(function (f) {
33        f(msg)
34      })
35    } else {
36      if (showHelpOnFail) yargs.showHelp('error')
37      if (msg) console.error(msg)
38      if (failMessage) {
39        if (msg) console.error('')
40        console.error(failMessage)
41      }
42      if (yargs.getExitProcess()) {
43        process.exit(1)
44      } else {
45        throw new Error(msg)
46      }
47    }
48  }
49
50  // methods for ouputting/building help (usage) message.
51  var usage
52  self.usage = function (msg) {
53    usage = msg
54  }
55
56  var examples = []
57  self.example = function (cmd, description) {
58    examples.push([cmd, description || ''])
59  }
60
61  var commands = []
62  self.command = function (cmd, description) {
63    commands.push([cmd, description || ''])
64  }
65  self.getCommands = function () {
66    return commands
67  }
68
69  var descriptions = {}
70  self.describe = function (key, desc) {
71    if (typeof key === 'object') {
72      Object.keys(key).forEach(function (k) {
73        self.describe(k, key[k])
74      })
75    } else {
76      descriptions[key] = desc
77    }
78  }
79  self.getDescriptions = function () {
80    return descriptions
81  }
82
83  var epilog
84  self.epilog = function (msg) {
85    epilog = msg
86  }
87
88  var wrap = windowWidth()
89  self.wrap = function (cols) {
90    wrap = cols
91  }
92
93  self.help = function () {
94    normalizeAliases()
95
96    var demanded = yargs.getDemanded(),
97      options = yargs.getOptions(),
98      keys = Object.keys(
99        Object.keys(descriptions)
100        .concat(Object.keys(demanded))
101        .concat(Object.keys(options.default))
102        .reduce(function (acc, key) {
103          if (key !== '_') acc[key] = true
104          return acc
105        }, {})
106      ),
107      ui = cliui({
108        width: wrap,
109        wrap: !!wrap
110      })
111
112    // the usage string.
113    if (usage) {
114      var u = usage.replace(/\$0/g, yargs.$0)
115      ui.div(u + '\n')
116    }
117
118    // your application's commands, i.e., non-option
119    // arguments populated in '_'.
120    if (commands.length) {
121      ui.div('Commands:')
122
123      commands.forEach(function (command) {
124        ui.div(
125          {text: command[0], padding: [0, 2, 0, 2], width: maxWidth(commands) + 4},
126          {text: command[1]}
127        )
128      })
129
130      ui.div()
131    }
132
133    // the options table.
134    var aliasKeys = (Object.keys(options.alias) || [])
135      .concat(Object.keys(yargs.parsed.newAliases) || [])
136
137    keys = keys.filter(function (key) {
138      return !yargs.parsed.newAliases[key] && aliasKeys.every(function (alias) {
139        return (options.alias[alias] || []).indexOf(key) === -1
140      })
141    })
142
143    var switches = keys.reduce(function (acc, key) {
144      acc[key] = [ key ].concat(options.alias[key] || [])
145      .map(function (sw) {
146        return (sw.length > 1 ? '--' : '-') + sw
147      })
148      .join(', ')
149
150      return acc
151    }, {})
152
153    if (keys.length) {
154      ui.div('Options:')
155
156      keys.forEach(function (key) {
157        var kswitch = switches[key]
158        var desc = descriptions[key] || ''
159        var type = null
160
161        if (~options.boolean.indexOf(key)) type = '[boolean]'
162        if (~options.count.indexOf(key)) type = '[count]'
163        if (~options.string.indexOf(key)) type = '[string]'
164        if (~options.normalize.indexOf(key)) type = '[string]'
165        if (~options.array.indexOf(key)) type = '[array]'
166
167        var extra = [
168            type,
169            demanded[key] ? '[required]' : null,
170            defaultString(options.default[key], options.defaultDescription[key])
171          ].filter(Boolean).join(' ')
172
173        ui.span(
174          {text: kswitch, padding: [0, 2, 0, 2], width: maxWidth(switches) + 4},
175          desc
176        )
177
178        if (extra) ui.div({text: extra, padding: [0, 0, 0, 2], align: 'right'})
179        else ui.div()
180      })
181
182      ui.div()
183    }
184
185    // describe some common use-cases for your application.
186    if (examples.length) {
187      ui.div('Examples:')
188
189      examples.forEach(function (example) {
190        example[0] = example[0].replace(/\$0/g, yargs.$0)
191      })
192
193      examples.forEach(function (example) {
194        ui.div(
195          {text: example[0], padding: [0, 2, 0, 2], width: maxWidth(examples) + 4},
196          example[1]
197        )
198      })
199
200      ui.div()
201    }
202
203    // the usage string.
204    if (epilog) {
205      var e = epilog.replace(/\$0/g, yargs.$0)
206      ui.div(e + '\n')
207    }
208
209    return ui.toString()
210  }
211
212  // return the maximum width of a string
213  // in the left-hand column of a table.
214  function maxWidth (table) {
215    var width = 0
216
217    // table might be of the form [leftColumn],
218    // or {key: leftColumn}}
219    if (!Array.isArray(table)) {
220      table = Object.keys(table).map(function (key) {
221        return [table[key]]
222      })
223    }
224
225    table.forEach(function (v) {
226      width = Math.max(v[0].length, width)
227    })
228
229    // if we've enabled 'wrap' we should limit
230    // the max-width of the left-column.
231    if (wrap) width = Math.min(width, parseInt(wrap * 0.5, 10))
232
233    return width
234  }
235
236  // make sure any options set for aliases,
237  // are copied to the keys being aliased.
238  function normalizeAliases () {
239    var options = yargs.getOptions(),
240    demanded = yargs.getDemanded()
241
242    ;(Object.keys(options.alias) || []).forEach(function (key) {
243      options.alias[key].forEach(function (alias) {
244        // copy descriptions.
245        if (descriptions[alias]) self.describe(key, descriptions[alias])
246        // copy demanded.
247        if (demanded[alias]) yargs.demand(key, demanded[alias].msg)
248
249        // type messages.
250        if (~options.boolean.indexOf(alias)) yargs.boolean(key)
251        if (~options.count.indexOf(alias)) yargs.count(key)
252        if (~options.string.indexOf(alias)) yargs.string(key)
253        if (~options.normalize.indexOf(alias)) yargs.normalize(key)
254        if (~options.array.indexOf(alias)) yargs.array(key)
255      })
256    })
257  }
258
259  self.showHelp = function (level) {
260    level = level || 'error'
261    console[level](self.help())
262  }
263
264  self.functionDescription = function (fn, defaultDescription) {
265    if (defaultDescription) {
266      return defaultDescription
267    }
268    var description = fn.name ? decamelize(fn.name, '-') : 'generated-value'
269    return ['(', description, ')'].join('')
270  }
271
272  // format the default-value-string displayed in
273  // the right-hand column.
274  function defaultString (value, defaultDescription) {
275    var string = '[default: '
276
277    if (value === undefined) return null
278
279    if (defaultDescription) {
280      string += defaultDescription
281    } else {
282      switch (typeof value) {
283        case 'string':
284          string += JSON.stringify(value)
285          break
286        case 'object':
287          string += JSON.stringify(value)
288          break
289        default:
290          string += value
291      }
292    }
293
294    return string + ']'
295  }
296
297  // guess the width of the console window, max-width 80.
298  function windowWidth () {
299    return wsize.width ? Math.min(80, wsize.width) : null
300  }
301
302  // logic for displaying application version.
303  var version = null
304  self.version = function (ver, opt, msg) {
305    version = ver
306  }
307
308  self.showVersion = function () {
309    if (typeof version === 'function') console.log(version())
310    else console.log(version)
311  }
312
313  return self
314}
315