├── 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
--------------------------------------------------------------------------------