UNPKG

klog

Version:
1,410 lines (1,182 loc) 39.1 kB
#!/usr/bin/env coffee # START HEADER COMMENTS ### Author: Billy Moon (http://billy.itaccess.org/) LICENSE: Copyright (c) 2012 by Billy Moon. All rights reserved. This module is free software; you can redistribute it and/or modify it under the MIT license The LICENSE file contains the full text of the license. ### # END HEADER COMMENTS ## Modules fs = require 'fs' exec = require("child_process").exec # Underscore Library _ = require('../lib/underscore-min.js') # MD5 Library md5 = require('../lib/md5.js').MD5.hex_md5 # Editor Library editor = require('../lib/editor.js') ## Functions parseArgs = -> ## alternative implementation idea based on creating a `command` class # # in_array = (needle, haystack)-> # for e in haystack # if e == needle then return true # false # cli = "show --type there -x 'this is' -c 'anything \" goes --here#$#$' --happen --thong -m this goes to the end -t happen" # cli = "add -t feature -p 2 -m theis is the message%" # rex = /-(m.+|-?(\S+))(\s+((['"]).+?\5|[^'"-]\S*))?/g # commands = # show: # mandatory: ['m'] # optional: ['x','c'] # class command # constructor: (cli)-> # m = cli.match /^\S+/ # @cmd = m[0] # @args = {} # for i in cli.match rex # m = i.match /--?(\S+)(\s+(['"]?)(.+)\3)?/ # console.log m # cmd = m[1] # if m[4] # params = m[4] # else # params = null # @args[cmd] = params # validate: -> # err = null # if ! commands[@cmd] # err = "command: `#{@cmd}` does not exist" # else # for e in @args # if ! in_array e, commands.mandatory # console.log "bad:" + e # else # console.log "good: " + e # err # Command line options options = s:'state' m:'message' e:'editor' t:'type' p:'priority' l:'label' # simple toggles, don't consume next argument switches = a:'all' d:'debug' # x:'exit' f:'force' r:'return' x:'plain' # no colours in output, and lean towards formatting suited to scripts args = process.argv o = {_:[],$0:[]} validOptions = [] for k, v of options validOptions.push v i = -2 na = false # next argument: false/opt/flag for arg in args if m = arg.match /^--(.+?)(=(.+))?$/ na = m[1] o[m[1]] = m[3] || true else if m = arg.match /^-(.+?)(=(.+))?$/ if na = options[m[1]] if na == 'message' o[na] = m[3] ? [m[3]] | [''] else o[na] = m[3] || true else if switches[m[1]] na = false o[switches[m[1]]] = m[3] || true else print 'Unknown flag: '+m[1] exit 1 else if ++i > 0 # ignore first two args which are node and app if na == 'message' o.message = [arg] else if na != false o[na] = arg else if o.message o.message.push arg else o._.push arg na = false else o['$0'].push arg if o.message o.message = o.message.join ' ' return o ## Utility functions. # Pad a string (with 0 or specified) pad = (e,t,n)-> n = n || "0" t = t || 2 while (""+e).length<t e=n+e e # return date in format yyyy-mm-dd_hh-ii-ss getDate = -> c = new Date() return c.getFullYear()+"-"+pad(c.getMonth()+1)+"-"+pad(c.getDate())+"_"+c.toLocaleTimeString().replace(/\D/g,'-')+"."+pad(c.getMilliseconds(),3) asDate = (datestring) -> new Date datestring.replace(/_/,'T').replace(/T(.+)-(.+)-/,"T$1:$2:") # Generate a system UID. This should be created with the username and # time included, such that collisions when running upon multiple systems # are unlikely. randomUID = -> # The values that feed into the filename. $uid = opts.date+"."+opts.email $uid = md5 $uid $uid = $uid.replace /(.{4}).+/, "$1" return $uid # Find and return an array of hashes, one for each existing bug. getBugs = -> if ! opts.path print """ This directory does not appear to have Klogs on! Put them on with: klog init or try `klog help` for more info """ exit() files = fs.readdirSync "#{opts.path+opts.store}" files.sort() $results = [] $number = 1 for file in files if file.match /\.log$/ $status = 'open' buffer = fs.readFileSync "#{opts.path+opts.store}#{file}" lines = buffer.toString().split /[\r\n]+/ # print content $priority = 0 $modified = null $body = [] for line in lines if m = line.match /^Title: (.*)/ $title = m[1] else if m = line.match /^Type: (.*)/ $type = m[1] else if m = line.match /^Priority: (.*)/ $priority = m[1] else if m = line.match /^Added: (.*)/ $added = m[1] else if m = line.match /^Modified: (.*)/ $modified = m[1] else if m = line.match /^Author: (.*)/ $author = m[1] else if m = line.match /^UID: (.*)/ $uid = m[1] else if m = line.match /^Status: (.*)/i $status = m[1] else $body.push "\r\n"+line if ! $modified $modified = $added $results.push file: file body: $body number: $number++ uid: $uid status: $status type: $type priority: $priority title: $title added: $added modified: $modified author: $author || 'unspecified' return $results # Print to console print = (txt) -> console.log txt # Get the data for a given bug, either by number of UID. getBugByUIDORNumber = ($arg) -> # Get all bugs. $bugs = getBugs() # For each one. for $possible in $bugs # If the argument was NNNN then look for that bug number. # strip lead bug identifier $arg = $arg.replace /^%/, '' if m = $arg.match /^([0-9]{1,3})$/i $bug = $possible if parseInt(m[1]) == $possible.number else # Otherwise look for it by UID $bug = $possible if $arg.toLowerCase() == $possible.uid.toLowerCase() if $bug return $bug print "Last resort, trying to search (open issues) for: #{glob.clrs.yellow}#{$arg}#{glob.clrs.reset}" bug = cmd.search return: true terms: $arg state: 'open' all: false if bug hl = if bug.status == 'open' then glob.clrs.green else glob.clrs.red cb = glob.clrs.bright ch = glob.clrs.yellow cr = glob.clrs.reset print "Found: %#{hl}#{bug.uid}#{glob.clrs.reset} [#{ch}#{bug.status}#{cr}] [#{ch+cb}#{bug.type}#{cr}] #{bug.title}" return bug # else # print bug print "Bug not found!!" exit 1 # Exit app with error code exit = (code) -> # print "#{glob.clrs.red}EXIT ~ with code: #{glob.clrs.bright}#{code}#{glob.clrs.reset}" # only if we are not in server mode if ! opts.server process.exit code # Open the given file with either the users editor, the systems editor, # or as a last resort vim or notepad depending on platform. editFile = (file) -> # Open the editor $editor = if opts.args.editor then opts.args.editor else if process.env.EDITOR then process.env.EDITOR else if opts.win then "notepad" else "vim" editor file, {} # exec "#{$editor} #{file}" # Remove the "# klog: " prefix from the given file. remove_comments = ($file) -> # Open the source file for reading. try buffer = fs.readFileSync $file catch e print "Failed to open #{$file}" exit content = buffer.toString().replace /^# klog:.*(\r\n|\n|\r)/mg, '' # Write the contents, removing any lines matching our marker-pattern fs.writeFileSync $file, content # Show the usage of this script and exit. usage = -> print ''' klog [options] sub-command [args] Available sub-commands: add - Add a new bug. append - Append text to an existing bug. Set type with -t, and use `.` as message for no message close - Change an open bug to closed. closed - List all currently closed bugs. edit - Allow a bug to be edited. delete - Allow a bug to be deleted. destroy - Destroys the whole klog storage folder (including all issue data!) init - Initialise the system. list|search - Display existing bugs. open - List all currently open bugs. reopen - Change a closed bug to open. view - Show all details about a specific bug. server - HTTP server displays bugs, and accepts commands Options: -f, --force - no confirmation when deleting -t, --type - issue type (default:bug) i.e. feature/enhance/task -m, --message - Use the given message rather than spawning an editor. -s, --state - Restrict matches when searching (open/closed). -a, --all - Search everywhere (type, and message), not just the title -p, --priority - Set the priority (`.` is replaced with `-`, so `.3` will result in `-3`) ''' # -e, --editor - Specify which editor to use. exit 0 hook = (action, file) -> if hooks[action] hooks[action].run file # Change the statues of an existing bug. Valid statuses are # "open" and "closed". changeBugState = ($value, $state) -> # Ensure the status is valid. if ! $state.match /^(open|closed)$/i print "Invalid status #{$state}" exit 1 # Get the bug. $bug = getBugByUIDORNumber $value # Ensure the bug isn't already in the specified state. if $bug.status == $state print "The bug is already #{$state}!\r\n" exit 1 # Now write out the new status section. content = """\r\n Modified: #{opts.date} Status: #{$state} """ fs.appendFileSync opts.path+opts.store+$bug.file, content add = asDate $bug.added mod = asDate opts.date print "("+Math.round(( (mod - add) / 1000 / 60 / 60 )*100)/100 + " hours after issue was added)" # print $bug # If there is a hook, run it. hook $state, $bug.file get_user_details = (callback) -> if opts.user && opts.email callback() else exec 'git config --get user.email', (se,so,e) -> if so.length opts.email = so.replace /[\r\n]+/, '' exec 'git config --get user.name', (se,so,e) -> if so.length opts.user = so.replace /[\r\n]+/, '' else opts.user = opts.email.replace /@.+$/, '' callback() else print """ Tried to get email address from Git, but could not determine using: \r\n\tgit config --get user.email\r\n It might be a good idea to set it with: \r\n\tgit config etc...\r\n """ print "Please enter your details... (leave blank to abort)" stdin = process.openStdin() process.stdout.write "Name: " stdin.addListener "data", (d) -> if ! opts.user && opts.user = d.toString().trim() process.stdout.write "Email: " else if ! opts.email && opts.email = d.toString().trim() process.stdin.destroy() callback() else print "Error: tried everything, still no name and email!" exit 1 get_confirmation = (callback, message) -> stdin = process.openStdin() process.stdout.write "Are you sure? [yep/nope]: " stdin.addListener "data", (d) -> if d.toString().match /y(e(p|s|ah))?/i callback() process.stdin.destroy() else if message print message process.stdin.destroy() exit 1 get_required = (items, final) -> stdin = process.stdin if ! items?.length stdin.pause() if ! opts.command.needs?.length delete opts.command.needs final() else if ! opts.args[items[0]] item = items.shift() if ! opts.args[item] && item process.stdout.write "#{item}: " stdin.resume() stdin.once 'data', (d) -> stdin.pause() line = d.toString().trim() if line opts.command.args[item] = line else items.unshift item get_required items, final ## Handlers for the commands. cmd = {} # Add a new bug. cmd.add = (args) -> print args # Make a "random" filename, with the same UID as the content. $uid = randomUID() $title = args.title $type = args.type || 'bug' $priority = args.priority || '0' $priority = $priority.replace /\./, '-' opts.args.file = "#{opts.date}.#{$uid}.log"; # Write our template to it opts.args.template = """ UID: #{$uid} Type: #{$type} Priority: #{$priority} Title: #{$title} Added: #{opts.date} Author: #{opts.user} \r\n """ # Status: open\r\n\r\n # If we were given a message, add it to the file, and return without # invoking the editor. if args.message fs.writeFileSync opts.path+opts.store+opts.args.file, opts.args.template + args.message print "added issue %#{glob.clrs.yellow}#{$uid}#{glob.clrs.reset}" # If there is a hook, run it. hook "add", opts.args.file return # Otherwise add the default text, and show it in an editor. # (ending newline helps in stripping the comments out later) else opts.args.template += """ # klog: # klog: Enter your bug report here; it is better to write too much than # klog: too little. # klog: # klog: Lines beginning with "# klog:" will be ignored, and removed, # klog: this file is saved. # klog:\r\n """ fs.writeFileSync opts.args.file, opts.args.template # Open the file in the users' editor. editFile opts.args.file # Once it was saved remove the lines that mention "# klog: " remove_comments opts.args.file print "added issue %#{glob.clrs.yellow}#{$uid}#{glob.clrs.reset}" # If there is a hook, run it. hook "add", opts.args.file # Open an editor with a new block appended to the end of the file. # This mostly means: # 1. find the file associated with a given bug. # 2. Append the new text. # 3. Allow the user to edit that file. cmd.append = (args) -> # Ensure we know what we're operating upon if ! args.id print """ You must specify a bug to append to, either by the UID, or via the number. For example to append text to bug number 3 you'd run: \r\n\tklog append 3\r\n """ exit 1 # Get the bug $bug = getBugByUIDORNumber args.id # If we were given a message add it, otherwise spawn the editor. # redundant when the message argument is required if args.message || args.type $out = "\r\n\r\nModified: #{opts.date}\r\n" if args.type $out += "Type: #{args.type}\r\n" if args.priority $out += "Priority: #{args.priority.replace /[\.]/, '-'}\r\n" if args.message != '.' $out += "#{args.message||''}" fs.appendFileSync opts.path+opts.store+$bug.file, $out return else $out = "\r\nModified: #{opts.date}\r\n\r\n" fs.appendFileSync opts.path+opts.store+$bug.file, $out # Allow the user to make the edits. editFile opts.path+opts.store+$bug.file ## BROKEN due to separate process ## # # Once it was saved remove the lines that mention "# klog: " # remove_comments opts.store+$bug.file # If there is a hook, run it. hook "append", $bug.file # Output a HTML page for the bugs. cmd.html = (args) -> # Get all bugs. $bugs = getBugs() # Open + closed bugs. $open = [] $closed = [] for $b in $bugs if $b.status.match /open/i $open.push $b else $closed.push $b # Counts $open_count = $open.length $closed_count = $closed.length out = """ <!DOCTYPE HTML> <html lang="en-US"> <head> <meta charset="UTF-8"> <title>klog : issue tracking and time management</title> <style type='text/css'> /* This is the<a href="#" class="button default inline">Default</a> action! <a href="#" class="button blue">Blue</a> */ .button { margin: 0 15px 15px 0; font-family: 'Lucida Grande', 'Helvetica Neue', sans-serif; font-size: 13px; display: inline-block; background-color: #f5f5f5; background-image: -webkit-linear-gradient(top,#f5f5f5,#f1f1f1); background-image: -moz-linear-gradient(top,#f5f5f5,#f1f1f1); background-image: -ms-linear-gradient(top,#f5f5f5,#f1f1f1); background-image: -o-linear-gradient(top,#f5f5f5,#f1f1f1); background-image: linear-gradient(top,#f5f5f5,#f1f1f1); color: #444; border: 1px solid #dcdcdc; -webkit-border-radius: 2px; -moz-border-radius: 2px; border-radius: 2px; cursor: default; font-size: 11px; font-weight: bold; text-align: center; height: 27px; line-height: 27px; min-width: 54px; padding: 0 8px; text-decoration: none; } .button.inline { margin: 0 .2em 0 .5em; } .button:hover { background-color: #F8F8F8; background-image: -webkit-linear-gradient(top,#f8f8f8,#f1f1f1); background-image: -moz-linear-gradient(top,#f8f8f8,#f1f1f1); background-image: -ms-linear-gradient(top,#f8f8f8,#f1f1f1); background-image: -o-linear-gradient(top,#f8f8f8,#f1f1f1); background-image: linear-gradient(top,#f8f8f8,#f1f1f1); border: 1px solid #C6C6C6; color: #333; -webkit-box-shadow: 0px 1px 1px rgba(0,0,0,.1); -moz-box-shadow: 0px 1px 1px rgba(0,0,0,.1); box-shadow: 0px 1px 1px rgba(0,0,0,.1); text-decoration: none; cursor: pointer; } /* blue */ .button.blue { background-color: #4D90FE; background-image: -webkit-linear-gradient(top,#4d90fe,#4787ed); background-image: -moz-linear-gradient(top,#4d90fe,#4787ed); background-image: -ms-linear-gradient(top,#4d90fe,#4787ed); background-image: -o-linear-gradient(top,#4d90fe,#4787ed); background-image: linear-gradient(top,#4d90fe,#4787ed); border: 1px solid #3079ED; color: white; } .button.blue:hover { border: 1px solid #2F5BB7; background-color: #357AE8; background-image: -webkit-linear-gradient(top,#4d90fe,#357ae8); background-image: -moz-linear-gradient(top,#4d90fe,#357ae8); background-image: -ms-linear-gradient(top,#4d90fe,#357ae8); background-image: -o-linear-gradient(top,#4d90fe,#357ae8); background-image: linear-gradient(top,#4d90fe,#357ae8); -webkit-box-shadow: 0 1px 1px rgba(0,0,0,.1); -moz-box-shadow: 0 1px 1px rgba(0,0,0,.1); box-shadow: 0 1px 1px rgba(0,0,0,.1); } /* red */ .button.red { background-color: #D14836; background-image: -webkit-linear-gradient(top,#dd4b39,#d14836); background-image: -moz-linear-gradient(top,#dd4b39,#d14836); background-image: -ms-linear-gradient(top,#dd4b39,#d14836); background-image: -o-linear-gradient(top,#dd4b39,#d14836); background-image: linear-gradient(top,#dd4b39,#d14836); border: 1px solid transparent; color: white; text-shadow: 0 1px rgba(0, 0, 0, 0.1); } .button.red:hover { background-color: #C53727; background-image: -webkit-linear-gradient(top,#dd4b39,#c53727); background-image: -moz-linear-gradient(top,#dd4b39,#c53727); background-image: -ms-linear-gradient(top,#dd4b39,#c53727); background-image: -o-linear-gradient(top,#dd4b39,#c53727); background-image: linear-gradient(top,#dd4b39,#c53727); } /* green */ .button.green { background-color: #3D9400; background-image: -webkit-linear-gradient(top,#3d9400,#398a00); background-image: -moz-linear-gradient(top,#3d9400,#398a00); background-image: -ms-linear-gradient(top,#3d9400,#398a00); background-image: -o-linear-gradient(top,#3d9400,#398a00); background-image: linear-gradient(top,#3d9400,#398a00); border: 1px solid #29691D; color: white; text-shadow: 0 1px rgba(0, 0, 0, 0.1); } .button.green:hover { background-color: #368200; background-image: -webkit-linear-gradient(top,#3d9400,#368200); background-image: -moz-linear-gradient(top,#3d9400,#368200); background-image: -ms-linear-gradient(top,#3d9400,#368200); background-image: -o-linear-gradient(top,#3d9400,#368200); background-image: linear-gradient(top,#3d9400,#368200); border: 1px solid #2D6200; text-shadow: 0 1px rgba(0, 0, 0, 0.3); } </style> <style type='text/css'> body{ font-family: century gothic; } .bug { background-color: #F7F7F7; border: 5px solid #666666; border-radius: 0.5em 0.5em 0.5em 0.5em; margin: 0.5em 0; padding: 0.3em 1em; } .bug h3{ font-size: 2em; margin: 0.2em 0; } #command-intro { padding-left: 0.5em; width: 3.4em; } input { background-color: black; border: medium none; color: silver; float: left; height: 2em; margin: 0; padding: 0; font-size: 1em; } h1, h2, h3, h4, h5, h6, p, ul{ clear: both; } #command{ width: 40em; } #execute{ border-left: 1px solid red; padding: 0 0.3em; } ul.nav{ padding-top: 1em; } ul.nav li{ float: left; list-style-type: none; margin: 0 1em 0 -1em; padding: 0; } ul.actions li{ float: left; list-style-type: none; } ul.actions{ padding: 0; } ul { margin: 0 0 1em; padding: 0 1em; } ul.attributes { color: #666666; list-style-type: circle; } .clear{ clear: both; } form{ border: 5px solid #666666; border-radius: 0.3em 0.3em 0.3em 0.3em; height: 2em; width: 49.05em; } </style> </head> <body onload="document.getElementById('command').focus()"> <h1>Klog : distributed issue tracking</h1> <form action='.' method='POST'> <input type="text" value="$ klog" readonly="readonly" name="intro" id="command-intro"> <input type="text" name="command" id="command"> <input type="submit" id="execute" value="execute!"> </form> <ul class='nav'> <li><a href='#open' class='button'>#{$open_count} : open bugs</a></li> <li><a href='#closed' class='button'>#{$closed_count} : closed bugs</a></li> </ul> <hr class='clear' /> <a name='open'></a> <h2 id="open">Open bugs</h2> """ for $b in $open out += """ <div class='bug'> <h3>#{$b.title}</h3> <ul class='attributes'> <li><strong>UID</strong>: #{$b.uid}</li> <li><strong>Added</strong>: #{$b.added}</li> <li><strong>Author</strong>: #{$b.author}</li> <li><strong>Type</strong>: #{$b.type}</li> <li><strong>Priority</strong>: #{$b.priority}</li> </ul> <p>#{$b.body.join "<br>\r\n<br>\r\n"}</p> <hr> <ul class='actions'> <li><a href='./?command=close #{$b.uid}' class='button blue'>Close</a></li> <li><a href='./?command=delete #{$b.uid} -f' class='button red' onclick='return confirm("Do you really want to delete this item?")'>Delete</a></li> </ul> <br class='clear' /> </div> """ out += """ <h2 id="closed">Closed bugs</h2> """ for $b in $closed out += """ <div class='bug'> <h3>#{$b.title}</h3> <ul class='attributes'> <li><strong>UID</strong>: #{$b.uid}</li> <li><strong>Added</strong>: #{$b.added}</li> <li><strong>Author</strong>: #{$b.author}</li> <li><strong>Type</strong>: #{$b.type}</li> <li><strong>Priority</strong>: #{$b.priority}</li> </ul> <p>#{$b.body.join "<br>\r\n<br>\r\n"}</p> <hr> <ul class='actions'> <li><a href='./?command=reopen #{$b.uid}' class='button green'>Re-open</a></li> <li><a href='./?command=delete #{$b.uid} -f' class='button red' onclick='return confirm("Do you really want to delete this item?")'>Delete</a></li> </ul> <br class='clear' /> </div> """ out += """ <div id="foot"> Generated by <a href="http://billymoon.github.com/klog/">klog</a>. </div> </body> </html> """ if args.return return out else print out # Search the existing bugs. # Here search means "match against title and status". Either of which # is optional. cmd.search = (args) -> # The search terms, if any. $terms = args.terms # Get all available bugs. $bugs = getBugs() # The state of the bugs the user is interested in. $state = args.state || 'all' # The type of the bugs the user is interested in. $type = args.type || "all" # The priority of the bugs the user is interested in. $priority = args.priority || "all" # catch unset priority, and reset to all if $priority == true then $priority = "all" if m = $priority.match /(.+)([+-])$/ $priority = m[1] direction = m[2] else direction = null # print "will search for `#{$terms}` with state `#{$state}` and type `#{$type}`" found = [] # For each bug for $bug in $bugs # If the user is being specific about status then # skip ones that don't match, as this is cheap. if $state != "all" and $state.toLowerCase() != $bug.status.toLowerCase() continue # If the user is being specific about type then # skip ones that don't match if $type != "all" and $type.toLowerCase() != $bug.type.toLowerCase() continue # If the user is being specific about priority then # skip ones that don't match if ($priority+'').match /\./ $priority = 0-$priority*10 # print [$priority, direction, parseInt($bug.priority)] $bug.priority = parseInt $bug.priority if $priority != "all" $priority = parseInt $priority if direction == '+' and $priority > $bug.priority continue else if direction == null and $priority != $bug.priority continue else if direction == '-' and $priority < $bug.priority continue # If there are search terms then search the title. # All terms must match. $match = 1 $b_body = $bug.body.join('').replace(/(\\.|[^\w\s])/g,'') # print $b_body pool = if args.all then $bug.title+$bug.type+$b_body else $bug.title if args.terms # there are $terms for $term in $terms.split /[ \t]+/ if ! pool.match new RegExp $term, 'i' $match = 0 # If we didn't find a match move on. continue unless $match found.push $bug if args.return && found.length == 1 return found[0] else output_cli found, 'priority' # output bugs list to command line, optionally with sorting output_cli = (bugs,sort)-> if sort == 'priority' bugs.sort (a,b)-> b.priority - a.priority else if sort == 'added' bugs.sort (a,b)-> bb = parseInt b.added.replace /\D/g,'' aa = parseInt a.added.replace /\D/g,'' bb - aa else if sort == 'modified' bugs.sort (a,b)-> bb = parseInt b.modified.replace /\D/g,'' aa = parseInt a.modified.replace /\D/g,'' bb - aa # console.log bugs out = [] for bug in bugs # Otherwise show a summary of the bug. # print sprintf "%-4s %s %-8s %-9s %s", "#".$b_number, $bug.uid, "[".$bug.status."]", "[".$bug.type."]", $bug.title . "\r\n"; # removed number: ##{$b_number} hl = if bug.status == 'open' then glob.clrs.green else glob.clrs.red cb = glob.clrs.bright ch = glob.clrs.yellow cr = glob.clrs.reset pr = if bug.priority > 1 then glob.clrs.bright+glob.clrs.yellow else if bug.priority > 0 then glob.clrs.yellow else if bug.priority < -1 then glob.clrs.gunmetal else glob.clrs.silver out.push "%#{hl}#{bug.uid}#{glob.clrs.reset} [#{pr}#{pad (bug.priority+'').replace(/^([1-9])/,'+$1'), 2, ' '}#{cr}] [#{ch}#{bug.status}#{cr}] [#{ch+cb}#{bug.type}#{cr}] #{bug.title}" print out.join "\r\n" # View a specific bug. # This means: # 1. Find the file associated with the bug. # 2. Open it and print it to the console. cmd.view = (args) -> $value = args.id # Ensure we know what we're operating upon if ! $value # there is not a $value print "You must specify a bug to view, either by the UID, or via the number.\r\n" print "\r\nFor example to view bug number 3 you'd run:\r\n" print "\tklog view 3\r\n\r\n"; print "Maybe a list of open bugs will help you:\r\n\r\n" cmd.search() print "\r\n" exit 1 # Get the bug. $bug = getBugByUIDORNumber $value # Show it to the console buffer = fs.readFileSync opts.path+opts.store + $bug.file print buffer.toString().replace /^(\w+): /gm, "#{glob.clrs.yellow}$1#{glob.clrs.reset}: " # Close a given bug. cmd.close = (args) -> # Get the bug. $value = args.id # Ensure we know what we're operating upon if ! $value # has $value print """ You must specify a bug to close, either by the UID, or via the number. For example to close bug number 3 you'd run: \r\n\tklog close 3\r\n\r\n """ exit 1 changeBugState $value, "closed" # Reopen a bug. cmd.reopen = (args) -> # Get the bug. $value = args.id # Ensure we know what we're operating upon if ! $value print """ You must specify a bug to reopen, either by the UID, or via the number. For example to reopen bug number 3 you'd run: \r\n\tklog reopen 3 """ exit 1 changeBugState $value, "open" # Allow a bug to be updated. # This mostly means: # 1. find the file associated with a given bug. # 2. Allow the user to edit that file. cmd.edit = (args) -> $value = args.id # Ensure we know what we're operating upon if ! $value print """ You must specify a bug to edit, either by the UID, or via the number. For example to edit bug number 3 you'd run: \r\n\tklog edit 3\r\n\r\n """ exit 1 # Find the bug. $bug = getBugByUIDORNumber $value # Edit the file the bug is stored in. editFile opts.path+opts.store+$bug.file # If there is a hook, run it. hook "edit", $bug.file # Allow a bug to be deleted. # This mostly means: # 1. find the file associated with a given bug. # 2. delete that file. cmd.delete = (args) -> cmd.view opts.command.args do_delete = -> $value = args.id # Ensure we know what we're operating upon if ! $value print """ You must specify a bug to delete, either by the UID, or via the number. For example to delete bug number 3 you'd run: \r\n\tklog delete 3\r\n """ exit 1 # Find the bug. $bug = getBugByUIDORNumber $value # Delete the file the bug is stored in. $file = $bug.file fs.unlinkSync opts.path+opts.store+$file # If there is a hook, run it. hook "delete", $bug.file if ! args.force print "About to delete this bug..." get_confirmation -> do_delete() , "Phew, that was close!" else do_delete() # Inititalise a new .klog directory. cmd.init = -> if ! fs.existsSync opts.store fs.mkdirSync opts.store opts.path = process.cwd()+'/' print "#{glob.clrs.gunmetal}Now you have klogs on#{glob.clrs.reset}#{glob.clrs.red}!#{glob.clrs.reset}" cmd.setup() else print "There is already a .klog/ directory present here" exit 1 cmd.destroy = (args) -> if args.force exec "rm -Rf #{opts.path+opts.store}" else print "This will destroy all issues. You must force this with `-f`." cmd.setup = -> if opts.user && opts.email settings = """ { "user":"#{opts.user || 'John Doe'}", "email":"#{opts.email || 'john@thedoughfactory.com'}" } """ fs.writeFileSync "#{opts.path+opts.store}.gitignore","local" fs.mkdirSync "#{opts.path+opts.store}local" fs.writeFileSync "#{opts.path+opts.store}local/settings.json", settings print "Wrote settings to local file: #{opts.path+opts.store}local/settings.json\r\n\r\n#{settings}\r\n" else get_user_details cmd.setup cmd.server = -> opts.server = true port = 1234 http = require 'http' qs = require 'querystring' url = require 'url' command = (data) -> # POST = JSON.parse POST # opts.args._.push data.command.split ' ' if data.command args = data.command.trim().split ' ' print args while process.argv.length > 2 process.argv.pop() _.each args, (v) -> process.argv.push v opts.date = getDate() main() http.createServer (req, res) -> out_html = -> # print req.body res.writeHead 200, 'Content-Type': 'text/html' opts.command.args.return = true res.end cmd.html opts.command.args if req.method == 'POST' body = '' req.on 'data', (data) -> body += data req.on 'end', -> POST = qs.parse body command POST out_html() else if req.method == 'GET' url_parts = url.parse req.url, true # print url_parts.query command url_parts.query out_html() .listen port print "Serving `#{opts.path}` at http://127.0.0.1:#{port}/" # parse opts.args and return command object # should validate required options, but not their values get_command = -> out = args: [] get_id = -> if id = opts.args._.shift() opts.args.id = id.replace /^%/, '' # valid commands defined as obejct tree commands = add: required: ['title','message'] valid: ['type','priority','label'] args: -> if opts.args._.length opts.args.title = opts.args._.join ' ' delete: required: ['id'] valid: ['force'] args: -> if id = opts.args._.shift() opts.args.id = id.replace /^%/, '' help: {} init: {} list: valid: ['type','state','terms','all','return','priority'] args: -> if subcommand == 'search' opts.args.all = true if opts.args._.length opts.args.terms = opts.args._.join ' ' console.log opts.args.terms open: required: ['state'] # auto populated valid: ['type','priority'] args: -> opts.args.state = 'open' closed: required: ['state'] # auto populated valid: ['type','priority'] args: -> opts.args.state = 'closed' view: required: ['id'] args: get_id edit: required: ['id'] valid: ['editor'] args: get_id append: required: ['id','message'] valid: ['type','priority'] args: get_id reopen: required: ['id'] args: get_id close: required: ['id'] args: get_id html: {} server: {} destroy: valid: ['force'] for command of commands if ! commands[command].valid commands[command].valid = [] commands[command].valid.push 'plain' # (no colours in output, and lean towards formatting suited to scripts) # print commands commands.search = commands.list # figure out what the command is, or assign `help` subcommand = opts.args._.shift() || 'help' # if no arguments command = commands[subcommand] || subcommand = 'help' # if invalid argument out.name = subcommand # parse remaining arguments according to subcommand if command.args then command.args() # required options if command.required for requirement in command.required if has = opts.args[requirement] out.args = {} unless out.args out.args[requirement] = has else out.needs = [] unless out.needs out.needs.push requirement # optional options if command.valid for valid in command.valid if has = opts.args[valid] out.args = {} unless out.args out.args[valid] = has # superfluous options for x of opts.args if x != '$0' && x != '_' if ! out.args[x] rejects = [] unless rejects rejects.push x if rejects message = "Error: unsupported option used" print message exit 1 return out # The main routine ************************************************************************************************ main = -> # Parse the command line options. opts.args = parseArgs() # Generate command from arguments opts.command = get_command() opts.command.name = opts.command.name.replace /^(open|closed|list)$/, 'search' # override colours if `plain` is chosen (probably should implement colours as plugin) if opts.command.args.plain for clr of glob.clrs glob.clrs[clr] = "" get_required opts.command.needs, -> # process.stdout.write "#{glob.clrs.red+glob.clrs.bright}Command: " # print opts.command # process.stdout.write "#{glob.clrs.reset}" # temporary fix of args opts.args._.unshift opts.command.name if opts.args.debug print opts.args if opts.args.exit exit 0 # Ensure we received an argument. if opts.args.help || ! opts.args._.length usage() exit 1 else opts.cmd = opts.args._.shift() if cmd[opts.command.name] cmd[opts.command.name] opts.command.args else usage() # Globals opts = ext: 'log' # file extension for data files date: getDate() store: '.klog/' win: process.platform == 'win32' # set project path path = process.cwd().split /\// for folder in path sep = if opts.win then "\\" else "/" tpath = (path.join sep)+sep if fs.existsSync "#{tpath+opts.store}" opts.path = tpath break path.pop() # Read settings (including `user` and `email`) if fs.existsSync "#{opts.path+opts.store}/local/settings.json" buffer = fs.readFileSync "#{opts.path+opts.store}/local/settings.json" settings = JSON.parse buffer.toString() opts = _.extend opts, settings glob = {} glob.clrs = { bright:"\u001b[1m", red:"\u001b[31m", green:"\u001b[32m", blue:"\u001b[34m", cyan:"\u001b[36m", magenta:"\u001b[35m", yellow:"\u001b[33m", black:"\u001b[30m", gunmetal:"\u001b[30m\u001b[1m", silver:"\u001b[37m", white:"\u001b[37m\u001b[1m", back_red:"\u001b[41m", back_green:"\u001b[42m", back_blue:"\u001b[44m", back_cyan:"\u001b[46m", back_magenta:"\u001b[45m", back_yellow:"\u001b[43m", back_black:"\u001b[40m", back_silver:"\u001b[47m", reset:"\u001b[m" } # get hooks and add them to the glogal hook object hooks = {} if fs.existsSync "#{opts.path+opts.store}hooks" fs.readdirSync("#{opts.path+opts.store}hooks").forEach (file) -> hooks[file.replace /\.\w+$/,''] = require "#{opts.path+opts.store}hooks/#{file}" # fire it up main()