├── README.md ├── USAGE ├── cmd.js ├── package.json └── screenshot.png /README.md: -------------------------------------------------------------------------------- 1 | # tdag 2 | 3 | > Manage tasks as a directed acyclic graph. 4 | 5 |
6 | 7 | ## Table of Contents 8 | 9 | - [Background](#background) 10 | - [Install](#install) 11 | - [Usage](#usage) 12 | - [Maintainers](#maintainers) 13 | - [Contribute](#contribute) 14 | - [License](#license) 15 | 16 | ## Stability: Experimental 17 | 18 | I'm still experimenting regularly with tdag: its current state may not reflect 19 | its future state much at all. Use with caution: commands or underlying database 20 | structure may change without warning! 21 | 22 | ## Background 23 | 24 | So many task management systems structure tasks as lists, or, in the better 25 | case, as trees. However, neither accurately captures the aspect of multiple 26 | parents. What we really need is a *directed acyclic graph* (DAG) to express the 27 | more complex relationships that tasks tend to have. 28 | 29 | I like modeling my problems as potentially deep graphs that capture the 30 | top-level problem statement, all the way down to the discrete concrete tasks I 31 | need to do to work up one level of abstraction to the next layer of tasks. 32 | Oftentimes completing one task ought to free up multiple tasks all over my task 33 | set, which a graph captures well. 34 | 35 | tdag offers the `tg` command, which provides quick command line access to your 36 | task graph. `tg` wants to be really good at understanding task blockages and 37 | dependencies, in order to excel at answering the question, "What things can I 38 | work on *now*?". 39 | 40 | ## Install 41 | 42 | ``` 43 | npm install -g tdag 44 | ``` 45 | 46 | ## Usage 47 | 48 | ``` 49 | USAGE: tg 50 | 51 | tg 52 | print all top-level tasks 53 | 54 | tg ID 55 | print the dependency tree rooted at ID 56 | 57 | tg add "fix hyperlog dataset issues" 58 | insert task at root 59 | 60 | tg add ID "regenerate corrupted indexes" 61 | add task that is a dependency of todo #ID 62 | 63 | tg ready 64 | print all tasks that are ready to be worked on 65 | 66 | tg done ID 67 | mark a task as done 68 | 69 | tg block ID 70 | tg unblock ID 71 | mark a task as blocked or unblocked 72 | ``` 73 | 74 | ## Example Use 75 | 76 | TODO: expand on this! 77 | 78 | ``` 79 | sww@figure8 $ tg ready 80 | 0 ° Try to use tdag for a real project 81 | 1 » foo 82 | 2 ✓ bar 83 | 2 ✓ bar 84 | 3 » finish readme 85 | 86 | sww@figure8 $ tg done 3 87 | 88 | sww@figure8 $ tg ready 89 | 0 ° Try to use tdag for a real project 90 | 1 » foo 91 | 2 ✓ bar 92 | 2 ✓ bar 93 | 3 ✓ finish readme 94 | 95 | ``` 96 | 97 | ## Background: Operation 98 | 99 | `tg` manipulates a file named `todo.json` in your current directory. This is 100 | nice for easy per-project use, but might not always be desirable and may change 101 | in the future. 102 | 103 | `tg` operates on a plain JSON file. This is convenient right now, but may change 104 | in the future. However, tdag will always operate on *human readable text 105 | formats*! 106 | 107 | ## License 108 | 109 | MIT 110 | -------------------------------------------------------------------------------- /USAGE: -------------------------------------------------------------------------------- 1 | USAGE: tg 2 | 3 | tg 4 | print all top-level tasks 5 | 6 | tg ID 7 | print the dependency tree rooted at ID 8 | 9 | tg add "fix hyperlog dataset issues" 10 | insert task at root 11 | 12 | tg add ID "regenerate corrupted indexes" 13 | add task that is a dependency of todo #ID 14 | 15 | tg ready 16 | print all tasks that are ready to be worked on 17 | 18 | tg done ID 19 | mark a task as done 20 | 21 | tg block ID 22 | tg unblock ID 23 | mark a task as blocked or unblocked 24 | 25 | -------------------------------------------------------------------------------- /cmd.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | var fs = require('fs') 4 | var path = require('path') 5 | var args = require('minimist')(process.argv) 6 | var defined = require('defined') 7 | var chalk = require('chalk') 8 | 9 | if (args.h || args.help) { 10 | exit(0) 11 | } 12 | 13 | if (args._[2] === 'add' || args._[2] === 'a') { 14 | var parentId = Number(args._[3]) 15 | var description = args._.slice(4).join(' ') 16 | if (isNaN(parentId)) { 17 | parentId = undefined 18 | description = args._.slice(3).join(' ') 19 | } 20 | 21 | var db = load() 22 | var idx = db.idx++ 23 | var task = { 24 | description: description, 25 | deps: [], 26 | state: 'todo' 27 | } 28 | 29 | if (parentId !== undefined) { 30 | var parent = db.tasks[parentId] 31 | parent.deps.push(idx) 32 | } 33 | 34 | db.tasks[idx] = task 35 | save(db) 36 | 37 | console.log(idx + ': ' + description) 38 | } 39 | 40 | else if (args._[2] === 'ready' || args._[2] === 'r') { 41 | var db = load() 42 | var tasks = getTopLevel(db) 43 | 44 | var indent = 0 45 | tasks.forEach(function (id) { 46 | printDepTree(db, id, { hideBlocked: true }) 47 | }) 48 | } 49 | 50 | else if (args._.length === 4 && args._[2] === 'done') { 51 | var id = args._[3] 52 | var db = load() 53 | var task = db.tasks[id] 54 | var state = getTaskState(db, id) 55 | if (state === 'ready') { 56 | task.state = 'done' 57 | save(db) 58 | } else { 59 | console.log('Cannot mark DONE: task is blocked!') 60 | } 61 | } 62 | 63 | else if (args._.length === 4 && args._[2] === 'block') { 64 | var id = args._[3] 65 | var db = load() 66 | var task = db.tasks[id] 67 | var state = getTaskState(db, id) 68 | if (state !== 'done') { 69 | task.state = 'blocked' 70 | save(db) 71 | } else { 72 | console.log('Cannot mark BLOCKED: task is already done!') 73 | } 74 | } 75 | 76 | else if (args._.length === 4 && args._[2] === 'unblock') { 77 | var id = args._[3] 78 | var db = load() 79 | var task = db.tasks[id] 80 | var state = getTaskState(db, id) 81 | if (state === 'blocked') { 82 | task.state = 'todo' 83 | save(db) 84 | } else { 85 | console.log('Cannot mark UNBLOCKED: task isn\'t blocked!') 86 | } 87 | } 88 | 89 | // look up a specific task by its ID 90 | else if (args._.length === 3) { 91 | var id = args._[2] 92 | var db = load() 93 | printDepTree(db, id) 94 | } 95 | 96 | // print top-level items 97 | else if (args._.length === 2) { 98 | var db = load() 99 | Object.keys(db.tasks).forEach(function (id) { 100 | var task = db.tasks[id] 101 | if (getParents(db, id).length === 0) { 102 | printDepTree(db, id) 103 | } 104 | }) 105 | } 106 | 107 | function exit (code) { 108 | fs.createReadStream(path.join(__dirname, 'USAGE')).pipe(process.stdout) 109 | process.stdout.on('end', function () { 110 | process.exit(code) 111 | }) 112 | } 113 | 114 | function load () { 115 | var file = defined(args.f, args.file, 'todo.json') 116 | if (fs.existsSync(file)) { 117 | var db = JSON.parse(fs.readFileSync(file)) 118 | db.index = {} 119 | db.index.parents = computeParents(db) 120 | return db 121 | } else { 122 | return { 123 | idx: 0, 124 | tasks: {} 125 | } 126 | } 127 | } 128 | 129 | function save (db) { 130 | var file = defined(args.f, args.file, 'todo.json') 131 | fs.writeFileSync('todo.json', JSON.stringify(db, null, 2)) 132 | } 133 | 134 | function computeParents (db) { 135 | var idx = {} 136 | Object.keys(db.tasks).forEach(function (id) { 137 | var task = db.tasks[id] 138 | if (!idx[id]) idx[id] = [] 139 | task.deps.forEach(function (did) { 140 | if (!idx[did]) idx[did] = [] 141 | idx[did].push(id) 142 | }) 143 | }) 144 | return idx 145 | } 146 | 147 | function getParents (db, pid) { 148 | return db.index.parents[pid] 149 | } 150 | 151 | function getTopLevel (db) { 152 | var res = [] 153 | Object.keys(db.tasks).forEach(function (id) { 154 | var task = db.tasks[id] 155 | if (getParents(db, id).length === 0) { 156 | res.push(id) 157 | } 158 | }) 159 | return res 160 | } 161 | 162 | function whitespace (num) { 163 | var res = '' 164 | for (var i=0; i < num; i++) { 165 | res += ' ' 166 | } 167 | return res 168 | } 169 | 170 | function getTaskState (db, id) { 171 | var task = db.tasks[id] 172 | 173 | function done (id) { 174 | return db.tasks[id].state === 'done' 175 | } 176 | 177 | function readySomewhere (id) { 178 | var task = db.tasks[id] 179 | if (task.state === 'done') return false 180 | else if (task.state === 'blocked') return false 181 | else if (task.deps.length === 0) return true 182 | else { 183 | var states = task.deps.map(function (id) { 184 | return getTaskState(db, id) 185 | }) 186 | return states.some((state) => state === 'ready' || state === 'semi-ready') 187 | } 188 | } 189 | 190 | if (task.state === 'blocked') { 191 | return 'blocked' 192 | } else if (task.state === 'todo' && task.deps.every(done)) { 193 | return 'ready' 194 | } else if (readySomewhere(id)) { 195 | return 'semi-ready' 196 | } else if (task.state === 'todo' && !task.deps.every(done)) { 197 | return 'blocked' 198 | } else if (task.state === 'done') { 199 | return 'done' 200 | } else { 201 | oops(1) 202 | } 203 | } 204 | 205 | function getStateSymbol (state) { 206 | if (state === 'ready') return chalk.green('»') 207 | else if (state === 'semi-ready') return chalk.bold.yellow('°') 208 | else if (state === 'done') return chalk.gray('✓') 209 | else if (state === 'blocked') return chalk.red('✖') 210 | else oops(2) 211 | } 212 | 213 | function getStateTextColorFn (state) { 214 | if (state === 'ready') return chalk.bold.green 215 | else if (state === 'semi-ready') return chalk.yellow 216 | else if (state === 'done') return chalk.gray 217 | else if (state === 'blocked') return chalk.bold.red 218 | else oops(3) 219 | } 220 | 221 | function oops (id) { 222 | throw new Error('oops, I did not consider this case! fix me! id = ' + id) 223 | } 224 | 225 | function printDepTree (db, id, opts) { 226 | opts = opts || {} 227 | 228 | var indent = 0 229 | print(id) 230 | 231 | function print (id) { 232 | var task = db.tasks[id] 233 | var state = getTaskState(db, id) 234 | 235 | if (state === 'done') { 236 | return 237 | } 238 | if (opts.hideBlocked && state === 'blocked') { 239 | return 240 | } 241 | 242 | var sigil = getStateSymbol(state) 243 | var text = getStateTextColorFn(state)(task.description) 244 | var padding = 4 - String(id).length 245 | console.log(whitespace(indent) + id + whitespace(padding) + sigil + ' ' + text) 246 | var origIndent = indent + 2 247 | task.deps.forEach(function (id) { 248 | indent = origIndent 249 | print(id) 250 | }) 251 | } 252 | } 253 | 254 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tdag", 3 | "description": "manage tasks as a directed acyclic graph", 4 | "author": "Kira Oakley ", 5 | "bin": { 6 | "tg": "cmd.js" 7 | }, 8 | "version": "1.0.0", 9 | "repository": { 10 | "url": "git://github.com/hackergrrl/tdag.git" 11 | }, 12 | "homepage": "https://github.com/hackergrrl/tdag", 13 | "bugs": "https://github.com/hackergrrl/tdag/issues", 14 | "main": "index.js", 15 | "scripts": { 16 | "lint": "standard" 17 | }, 18 | "keywords": [], 19 | "dependencies": { 20 | "chalk": "^1.1.3", 21 | "defined": "^1.0.0", 22 | "minimist": "^1.2.0" 23 | }, 24 | "devDependencies": { 25 | "standard": "~10.0.0" 26 | }, 27 | "license": "ISC" 28 | } 29 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackergrrl/tdag/2d61331412fe6c9ca2b03a25d8231ab3113a1425/screenshot.png --------------------------------------------------------------------------------