fogbugz.js
Version:
FogBugz client for nodejs.
314 lines (274 loc) • 8.59 kB
text/coffeescript
fs = require('fs')
fogbugz = require('../fogbugz')
read = require('read')
askfor = require('askfor')
Table = require('cli-table')
memo = require('memoizee')
iz = require('iz')
Q = require('q')
print = require('node-more')
fcall = (f) -> f()
# promise duck
done =
then: fcall
done: fcall
fail: fcall
# instance of fogbugz client
fb = null
currentCase = null
lastCases = []
help = ->
cmds =
[
['ls', 'list active cases from current filter'],
['ls f', 'list available filters'],
['ls p', 'list available projects'],
['ls m', 'list available milestones']
['ls u', 'list available users'],
['ls c', 'list available case categories'],
['ls s', 'list available case statuses'],
['ls l', 'list available priorities'],
['ps', 'list available projects'],
['ms', 'list available milestones'],
['us', 'list available users'],
['u id', 'print user info'],
['settings', 'prints user settings'],
['search q', 'searches cases by specified q'],
['take #case [comment]', 'assign given case to logon user'],
['resolve [#case] [comment]', 'resolves current or given case']
['close #case [comment]', 'close given case'],
['reopen #case [comment]', 'reopen given case'],
['assign #case userId [comment]', 'assign given case to given user'],
['kick #case [comment]', 'return given case back to team'],
['log [#case] [comment]', 'logs specified comment to given case'],
['q[uit]', 'exit from this shell'],
['exit', 'exit from this shell'],
['help', 'print this command list'],
]
console.log 'commands:'
col = Math.max.apply(null, cmds.map (x) -> x[0].length)
list = cmds.map (x) -> ' ' + padr(x[0], col) + ' - ' + x[1]
list.forEach (s) -> console.log(s)
done
padr = (s,len) -> s + Array(len-s.length).join ' '
repl = -> read {prompt: '>>> '}, (err, l) -> printError(run(l)).done(repl)
# evals specified line
run = (l) ->
args = []
rx = /('[^']*')|("[^"]*")|(\w|\d)+/g
while (m = rx.exec(l)) != null
args.push unquote m[0]
cmd = args[0]
switch cmd
when 'help' then do help
when 'ls' then ls args
when 'ps' then projects().then printProjects
when 'ms' then ms args
when 'us' then users().then printUsers
when 'search' then search args
when 'take' then take args
when 'resolve' then resolve args
when 'assign' then assign args
when 'close' then close args
when 'reopen' then reopen args
when 'kick' then kick args
when 'log' then log args
when 'u' then resolveUser(args[1]).then((x) -> [x]).then(printUsers)
# todo pretty printing of user settings
when 'settings'
fb.userSettings().then (settings) ->
printTable [settings]
when 'q' then process.exit 0
when 'quit' then process.exit 0
when 'exit' then process.exit 0
else unkcmd cmd
unquote = (s) ->
if s[0] == '"' or s[0] == "'" then s.substr 1, s.length - 2 else s
unkcmd = (cmd) ->
console.log 'unknown command: %s', cmd
done
# CONFIG
cfgfile = __dirname + '/fbsh.conf.json'
auth = (cb) ->
if fs.existsSync cfgfile
options = require(cfgfile)
# TODO check token
if options.token then start options, false, cb else askcfg cb
else askcfg cb
askcfg = (cb) ->
askfor ['fogbugz url', 'user', 'password'], (answers) ->
options =
url: answers['fogbugz url']
user: answers.user,
password: answers.password
start options, true, cb
start = (options, updateConfig, cb) ->
p = fogbugz(options)
p.fail (error) ->
console.log(error)
# TODO auth again
process.exit(-1)
p.done (client) ->
if updateConfig
delete options.password
options.token = client.token
fs.writeFileSync(cfgfile, toJson(options), 'utf8')
fb = client
cb()
# returns memoized Fb method
fbmemo = (method) ->
fn = null
->
fn = memo(fb[method]) unless fn != null
fn.apply fb, [].slice.call(arguments)
filters = fbmemo('filters')
projects = fbmemo('projects')
users = fbmemo('people')
milestones = fbmemo('milestones')
categories = fbmemo('categories')
statuses = fbmemo('statuses')
priorities = fbmemo('priorities')
# list command handler
ls = (args) ->
switch args[1]
when 'f' then filters().then printTable
when 'p' then projects().then printProjects
when 'u' then users().then printUsers
when 'm' then milestones().then printMilestones
when 'c' then categories().then printTable
when 's' then statuses().then printTable
when 'l' then priorities().then printTable
else
id = if args[1] then parseInt(args[1], 10) else NaN
if isNaN id
listActiveCases()
else
caseObj = lastCases.filter((x) -> x.id == id)[0]
if caseObj then fb.events(caseObj.id).then printEvents else done
listActiveCases = ->
fb.search()
.then(filterActiveCases)
.then((list) -> lastCases = list)
.then printCases
# TODO do not hardcode active status
filterActiveCases = (list) -> list.filter (x) -> x.status.id == 1
# listing milestone
ms = (args) ->
if args.length >= 2
return resolveMilestone(args[1]).then (m) -> m.cases().then(printCases)
return milestones().then printMilestones
resolveMilestone = (id) ->
n = parseInt(id, 10)
return milestones().then (list) ->
f = list.filter (m) -> m.id == n || m.name == id
return fb.milestone f[0] if f.length == 1
return {cases: -> Q []}
# search command handler
search = (args) ->
# TODO intelligent search
fb.search(args[2]).then(printCases)
workflowFile = __dirname + '/workflow.json'
workflow = ->
return require workflowFile if fs.existsSync workflowFile
return {}
# take command handler
take = (args) ->
fb.take(args[1], args[2]).then -> currentCase = args[1]
parseArgs1 = (args) ->
id = parseInt(args[1], 10)
comment = args[2] || ''
if isNaN id
comment = args[1]
return Q.reject('no taken case') if !currentCase
id = currentCase
return Q({id: id, comment: comment})
# resolve command handler
resolve = (args) ->
parseArgs1(args).then (c) ->
wf = workflow()
if not wf.resolve then return Q.reject('no resolve workflow')
fb.resolve
id: c.id
comment: c.comment
status: wf.resolve.status
user: wf.resolve.user
# close command handler
close = (args) ->
parseArgs1(args).then (c) ->
fb.close c.id, c.comment
# reopen command handler
reopen = (args) ->
fb.close args[1], args[2]
# assign command handler
assign = (args) ->
cid = parseInt(args[1])
return error('expected case number') if isNaN cid
resolveUser(args[2]).then (u) ->
fb.assign cid, u.id, args[3]
# kick command handler
kick = (args) ->
parseArgs1(args).then (c) ->
wf = workflow()
resolveUser(wf.kick).then (u) ->
fb.assign c.id, u.id, c.comment
resolveUser = (id) ->
if !id then return error('user id is not specified')
users().then (list) ->
return (list.filter (u) -> u.email.indexOf(id) >= 0)[0] if iz.email(id)
return (list.filter (u) -> u.id == id)[0] if iz.int(id)
id = id.toLowerCase()
return (list.filter (u) ->
u.name.toLowerCase() == id || shortName(u).toLowerCase() == id)[0]
# log command impl
log = (args) ->
parseArgs1(args).then (c) ->
fb.log c.id, c.comment
# utils
isfn = (x) -> typeof x == 'function'
toJson = (x) -> JSON.stringify x, null, 2
printJson = -> [].slice.call(arguments, 0).forEach (x) -> print(toJson x)
printError = (promise) ->
promise.fail error
promise
error = (msg) ->
console.log msg
done
unwrap = (x) ->
if x.id && x.name then x.name
else if typeof x is 'string' then x else JSON.stringify(x)
printTable = (list, keys) ->
list = list.filter (x) -> x != null and x != undefined
if list.length == 0 then return done
head = (keys || Object.keys(list[0])).filter (x) -> !isfn list[0][x]
table = new Table({head: head})
table.push.apply table, list.map (x) -> head.map (k) -> unwrap x[k]
print table.toString()
printProjects = (list) -> printTable list, ['id', 'name', 'email', 'owner']
printMilestones = (list) -> printTable list, ['id', 'name', 'project']
printUsers = (list) -> printTable list, ['id', 'name', 'email']
printCases = (list) ->
if list.length == 0 then return done
table = new Table
head: ['#', 'assignee', 'title', 'tags'],
colWidths: [8, 10, 80, 10]
table.push.apply table, list.map (x) -> [
x.id || 0,
shortName(x.assignee),
x.title || '',
(x.tags || []).join(', ')
]
print table.toString()
printEvents = (list) ->
lines = list.map (e) ->
shortName(e.person) + ' ' +
ago(e.date) + ': ' +
(e.text || e.description)
print lines.join '\n'
shortName = (user) ->
return '' if !user || !user.name
arr = user.name.split(' ')
return user.name if arr.length <= 1 || arr[0].length <= 2
return arr[0] + arr[1].substr(0, 1)
ago = (d) -> require('pretty-date').format(d)
# run the shell
auth repl