@basementuniverse/kanbn 2.0.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/coverage/tmp/coverage-916017-1788028598821-0.json +1 -0
- package/coverage/tmp/{coverage-214293-1787777184569-0.json → coverage-916018-1788028597179-0.json} +1 -1
- package/coverage/tmp/coverage-916036-1788028598796-0.json +1 -0
- package/docs/actions.md +337 -0
- package/docs/advanced-configuration.md +55 -0
- package/docs/commands/add.txt +17 -1
- package/docs/commands/archive.txt +11 -0
- package/docs/commands/board.txt +9 -0
- package/docs/commands/boards.txt +34 -0
- package/docs/commands/burndown.txt +7 -0
- package/docs/commands/comment.txt +13 -1
- package/docs/commands/contributors.txt +58 -0
- package/docs/commands/edit.txt +18 -1
- package/docs/commands/find.txt +39 -1
- package/docs/commands/gantt.txt +6 -0
- package/docs/commands/help.txt +2 -0
- package/docs/commands/history.txt +6 -0
- package/docs/commands/init.txt +13 -0
- package/docs/commands/move.txt +26 -0
- package/docs/commands/remove.txt +22 -1
- package/docs/commands/rename.txt +5 -0
- package/docs/commands/restore.txt +12 -0
- package/docs/commands/sort.txt +23 -0
- package/docs/commands/sprint.txt +9 -0
- package/docs/commands/status.txt +10 -1
- package/docs/commands/task.txt +9 -0
- package/docs/commands/validate.txt +29 -1
- package/docs/contributors.md +145 -0
- package/docs/filtering-and-sorting.md +60 -3
- package/docs/index-structure.md +145 -11
- package/docs/index.md +6 -2
- package/docs/multiple-boards.md +259 -0
- package/docs/quick-start.md +21 -1
- package/docs/task-structure.md +31 -3
- package/example/README.md +23 -0
- package/example/advanced/kanbn.yml +65 -0
- package/example/boards/.kanbn/design.md +31 -0
- package/example/boards/.kanbn/index.md +44 -0
- package/example/boards/.kanbn/tasks/add-usage-alert-emails.md +19 -0
- package/example/boards/.kanbn/tasks/build-tenant-settings-page.md +43 -0
- package/example/boards/.kanbn/tasks/create-organization-switcher.md +44 -0
- package/example/boards/.kanbn/tasks/design-onboarding-checklist.md +22 -0
- package/example/boards/.kanbn/tasks/refresh-marketing-site.md +28 -0
- package/example/boards/.kanbn/tasks/ship-billing-portal.md +29 -0
- package/package.json +9 -7
- package/routes/add.json +40 -12
- package/routes/archive.json +14 -2
- package/routes/board.json +11 -3
- package/routes/boards.json +30 -0
- package/routes/burndown.json +23 -7
- package/routes/comment.json +19 -5
- package/routes/contributors.json +18 -0
- package/routes/edit.json +37 -11
- package/routes/find.json +37 -12
- package/routes/gantt.json +20 -6
- package/routes/history.json +39 -25
- package/routes/init.json +3 -1
- package/routes/move.json +24 -6
- package/routes/remove.json +18 -3
- package/routes/rename.json +11 -3
- package/routes/restore.json +14 -2
- package/routes/sort.json +40 -11
- package/routes/sprint.json +14 -4
- package/routes/status.json +23 -7
- package/routes/task.json +10 -2
- package/routes/validate.json +18 -5
- package/skills/kanbn-plan/SKILL.md +10 -1
- package/skills/kanbn-replan/SKILL.md +6 -1
- package/src/actions.js +904 -0
- package/src/board.js +25 -2
- package/src/controller/add.js +72 -55
- package/src/controller/archive.js +9 -4
- package/src/controller/board.js +21 -13
- package/src/controller/boards.js +140 -0
- package/src/controller/burndown.js +14 -7
- package/src/controller/comment.js +13 -6
- package/src/controller/contributors.js +166 -0
- package/src/controller/edit.js +48 -18
- package/src/controller/find.js +87 -13
- package/src/controller/gantt.js +13 -6
- package/src/controller/history.js +13 -6
- package/src/controller/init.js +39 -4
- package/src/controller/move.js +153 -25
- package/src/controller/remove.js +73 -13
- package/src/controller/rename.js +8 -4
- package/src/controller/restore.js +24 -8
- package/src/controller/sort.js +59 -3
- package/src/controller/sprint.js +31 -7
- package/src/controller/status.js +8 -4
- package/src/controller/task.js +34 -10
- package/src/controller/validate.js +197 -7
- package/src/git-user-name.js +9 -0
- package/src/git-user.js +55 -0
- package/src/main.d.ts +387 -9
- package/src/main.js +2757 -113
- package/src/parse-index.js +219 -17
- package/src/parse-task.js +16 -0
- package/src/utility.js +274 -0
- package/coverage/tmp/coverage-214292-1787777191526-0.json +0 -1
package/src/board.js
CHANGED
|
@@ -55,6 +55,23 @@ module.exports = (() => {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Render the simple tasks in a column - lines in the board file that aren't task links. They have
|
|
60
|
+
* no fields to interpolate and take no part in filtering or sorting, so they're shown dimmed at
|
|
61
|
+
* the end of the column, and only in the default layout where the columns on screen are the
|
|
62
|
+
* columns in the file
|
|
63
|
+
* @param {object} index
|
|
64
|
+
* @param {string} columnName
|
|
65
|
+
* @return {string[]} The rendered simple tasks
|
|
66
|
+
*/
|
|
67
|
+
function getSimpleTaskStrings(index, columnName) {
|
|
68
|
+
return ((index.columnContent || {})[columnName] || [])
|
|
69
|
+
.filter(entry => !entry.block && !!entry.text && entry.text.indexOf('\n') === -1)
|
|
70
|
+
|
|
71
|
+
// A caret starts a markup sequence in terminal-kit, so any in the user's own text are escaped
|
|
72
|
+
.map(entry => `^-${entry.text.replace(/\^/g, '^^')}^:`);
|
|
73
|
+
}
|
|
74
|
+
|
|
58
75
|
/**
|
|
59
76
|
* Get a column heading with icons
|
|
60
77
|
* @param {object} index
|
|
@@ -166,7 +183,13 @@ module.exports = (() => {
|
|
|
166
183
|
table.push(board.headings.map(heading => heading.heading));
|
|
167
184
|
board.lanes.forEach(lane => table.push(
|
|
168
185
|
[lane.name],
|
|
169
|
-
lane.columns.map(column
|
|
186
|
+
lane.columns.map((column, i) => {
|
|
187
|
+
const cell = column.map(task => getTaskString(index, task));
|
|
188
|
+
if (view === null) {
|
|
189
|
+
cell.push(...getSimpleTaskStrings(index, board.headings[i].name));
|
|
190
|
+
}
|
|
191
|
+
return cell.join(TASK_SEPARATOR);
|
|
192
|
+
})
|
|
170
193
|
));
|
|
171
194
|
|
|
172
195
|
// Display as a table
|
|
@@ -178,7 +201,7 @@ module.exports = (() => {
|
|
|
178
201
|
borderChars: 'lightRounded',
|
|
179
202
|
borderAttr: { color: 'grey' },
|
|
180
203
|
textAttr: { color: 'white', bgColor: 'default' },
|
|
181
|
-
width: term.width,
|
|
204
|
+
width: utility.terminalWidth(term.width),
|
|
182
205
|
fit: true
|
|
183
206
|
}
|
|
184
207
|
);
|
package/src/controller/add.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
const
|
|
1
|
+
const workspace = require('../main');
|
|
2
2
|
const utility = require('../utility');
|
|
3
3
|
const inquirer = require('inquirer');
|
|
4
4
|
const fuzzy = require('fuzzy');
|
|
5
5
|
const chrono = require('chrono-node');
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
// The boards this command targets - set from --board before anything else runs
|
|
8
|
+
let kanbn = workspace;
|
|
9
|
+
let boardTargets = [];
|
|
7
10
|
|
|
8
11
|
inquirer.registerPrompt('datepicker', require('inquirer-datepicker'));
|
|
9
12
|
inquirer.registerPrompt('recursive', require('inquirer-recursive'));
|
|
@@ -15,9 +18,11 @@ inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt'))
|
|
|
15
18
|
* @param {string[]} taskIds
|
|
16
19
|
* @param {string} columnName
|
|
17
20
|
* @param {string[]} columnNames
|
|
21
|
+
* @param {?string} currentUser
|
|
22
|
+
* @param {object[]} contributors
|
|
18
23
|
* @return {Promise<any>}
|
|
19
24
|
*/
|
|
20
|
-
async function interactiveCreateTask(taskData, taskIds, columnName, columnNames) {
|
|
25
|
+
async function interactiveCreateTask(taskData, taskIds, columnName, columnNames, currentUser, contributors) {
|
|
21
26
|
const dueDateExists = (
|
|
22
27
|
'metadata' in taskData &&
|
|
23
28
|
'due' in taskData.metadata &&
|
|
@@ -123,13 +128,12 @@ async function interactiveCreateTask(taskData, taskIds, columnName, columnNames)
|
|
|
123
128
|
default: false,
|
|
124
129
|
when: answers => !assignedExists
|
|
125
130
|
},
|
|
126
|
-
{
|
|
127
|
-
type: 'input',
|
|
131
|
+
utility.assignedPrompt({
|
|
128
132
|
name: 'assigned',
|
|
129
133
|
message: 'Assigned to:',
|
|
130
|
-
default: assignedExists ? taskData.metadata.assigned :
|
|
134
|
+
default: assignedExists ? taskData.metadata.assigned : (currentUser || ''),
|
|
131
135
|
when: answers => answers.setAssigned || assignedExists
|
|
132
|
-
},
|
|
136
|
+
}, contributors),
|
|
133
137
|
{
|
|
134
138
|
type: 'recursive',
|
|
135
139
|
name: 'subTasks',
|
|
@@ -235,15 +239,26 @@ async function interactiveAddUntrackedTasks(untrackedTasks, columnName, columnNa
|
|
|
235
239
|
* @param {object} taskData
|
|
236
240
|
* @param {string} columnName
|
|
237
241
|
*/
|
|
238
|
-
function createTask(taskData, columnName) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
242
|
+
async function createTask(taskData, columnName) {
|
|
243
|
+
try {
|
|
244
|
+
|
|
245
|
+
// The task is created once, on the first target board, and then added to the others - one task
|
|
246
|
+
// file, referenced by however many boards were named
|
|
247
|
+
const [first, ...rest] = boardTargets;
|
|
248
|
+
const taskId = await first.kanbn.createTask(taskData, columnName);
|
|
249
|
+
console.log(
|
|
250
|
+
`Created task "${taskId}" in column "${columnName}"` +
|
|
251
|
+
(boardTargets.length > 1 || !first.main ? ` on board "${first.slug}"` : '')
|
|
252
|
+
);
|
|
253
|
+
utility.showActionWarnings(first.kanbn);
|
|
254
|
+
for (const target of rest) {
|
|
255
|
+
await target.kanbn.addTaskToBoard(taskId, target.columnName);
|
|
256
|
+
console.log(`Added task "${taskId}" to column "${target.columnName}" on board "${target.slug}"`);
|
|
257
|
+
utility.showActionWarnings(target.kanbn);
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
245
260
|
utility.error(error);
|
|
246
|
-
}
|
|
261
|
+
}
|
|
247
262
|
}
|
|
248
263
|
|
|
249
264
|
/**
|
|
@@ -252,50 +267,44 @@ function createTask(taskData, columnName) {
|
|
|
252
267
|
* @param {string} columnName
|
|
253
268
|
*/
|
|
254
269
|
async function addUntrackedTasks(untrackedTasks, columnName) {
|
|
255
|
-
for (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
270
|
+
for (const target of boardTargets) {
|
|
271
|
+
|
|
272
|
+
// The column chosen interactively or on the command line applies to the first board; the others
|
|
273
|
+
// use whatever resolveBoardTargets() paired them with
|
|
274
|
+
const targetColumn = target === boardTargets[0] ? columnName : target.columnName;
|
|
275
|
+
for (let untrackedTask of untrackedTasks) {
|
|
276
|
+
try {
|
|
277
|
+
await target.kanbn.addUntrackedTaskToIndex(untrackedTask, targetColumn);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
utility.error(error);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
261
282
|
}
|
|
283
|
+
console.log(
|
|
284
|
+
`Added ${untrackedTasks.length} task${untrackedTasks.length !== 1 ? 's' : ''} ` +
|
|
285
|
+
`to column "${targetColumn}"` + (boardTargets.length > 1 || !target.main ? ` on board "${target.slug}"` : '')
|
|
286
|
+
);
|
|
262
287
|
}
|
|
263
|
-
console.log(
|
|
264
|
-
`Added ${untrackedTasks.length} task${untrackedTasks.length !== 1 ? 's' : ''} to column "${columnName}"`
|
|
265
|
-
);
|
|
266
288
|
}
|
|
267
289
|
|
|
268
290
|
module.exports = async args => {
|
|
269
291
|
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
292
|
+
// Work out which boards this command targets, and which column to use on each. -b is repeatable,
|
|
293
|
+
// so one task can be created and placed on several boards at once
|
|
294
|
+
const targets = await utility.resolveBoardTargets(workspace, args);
|
|
295
|
+
if (targets === null) {
|
|
273
296
|
return;
|
|
274
297
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
index = await kanbn.getIndex();
|
|
280
|
-
} catch (error) {
|
|
281
|
-
utility.error(error);
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
const columnNames = Object.keys(index.columns);
|
|
285
|
-
if (!columnNames.length) {
|
|
286
|
-
utility.error('No columns defined in the index\nTry running {b}kanbn init -c "column name"{b}');
|
|
287
|
-
return;
|
|
298
|
+
boardTargets = targets;
|
|
299
|
+
kanbn = targets[0].kanbn;
|
|
300
|
+
for (const target of boardTargets) {
|
|
301
|
+
target.main = await target.kanbn.isMainBoard();
|
|
288
302
|
}
|
|
303
|
+
utility.showColumnFallbackNotices(boardTargets, kanbn.isVerbose(targets[0].index));
|
|
289
304
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
columnName = utility.strArg(args.column);
|
|
294
|
-
if (columnNames.indexOf(columnName) === -1) {
|
|
295
|
-
utility.error(`Column "${columnName}" doesn't exist`);
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
305
|
+
const index = targets[0].index;
|
|
306
|
+
const columnNames = Object.keys(index.columns);
|
|
307
|
+
const columnName = targets[0].columnName;
|
|
299
308
|
|
|
300
309
|
// Add untracked file(s)
|
|
301
310
|
if (args.untracked) {
|
|
@@ -393,10 +402,12 @@ module.exports = async args => {
|
|
|
393
402
|
|
|
394
403
|
// Assigned
|
|
395
404
|
if (args.assigned) {
|
|
396
|
-
const gitUsername = getGitUsername();
|
|
397
405
|
if (args.assigned === true) {
|
|
398
|
-
|
|
399
|
-
|
|
406
|
+
|
|
407
|
+
// A bare --assigned means "me", canonicalised against the contributors list if there is one
|
|
408
|
+
const currentUser = await kanbn.currentUser();
|
|
409
|
+
if (currentUser) {
|
|
410
|
+
taskData.metadata.assigned = currentUser;
|
|
400
411
|
}
|
|
401
412
|
} else {
|
|
402
413
|
taskData.metadata.assigned = utility.strArg(args.assigned);
|
|
@@ -501,7 +512,14 @@ module.exports = async args => {
|
|
|
501
512
|
|
|
502
513
|
// Create task interactively
|
|
503
514
|
if (args.interactive) {
|
|
504
|
-
interactiveCreateTask(
|
|
515
|
+
interactiveCreateTask(
|
|
516
|
+
taskData,
|
|
517
|
+
taskIds,
|
|
518
|
+
columnName,
|
|
519
|
+
columnNames,
|
|
520
|
+
await kanbn.currentUser(),
|
|
521
|
+
await kanbn.getContributors()
|
|
522
|
+
)
|
|
505
523
|
.then(answers => {
|
|
506
524
|
taskData.name = answers.name;
|
|
507
525
|
if ('description' in answers) {
|
|
@@ -534,8 +552,7 @@ module.exports = async args => {
|
|
|
534
552
|
type: relation.type
|
|
535
553
|
}));
|
|
536
554
|
}
|
|
537
|
-
|
|
538
|
-
createTask(taskData, columnName);
|
|
555
|
+
createTask(taskData, answers.column);
|
|
539
556
|
})
|
|
540
557
|
.catch(error => {
|
|
541
558
|
utility.error(error);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
const
|
|
1
|
+
const workspace = require('../main');
|
|
2
2
|
const utility = require('../utility');
|
|
3
3
|
|
|
4
|
+
// The board this command targets - set from --board before anything else runs
|
|
5
|
+
let kanbn = workspace;
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* Archive a task
|
|
6
9
|
* @param {string} taskId
|
|
@@ -10,6 +13,7 @@ function archiveTask(taskId) {
|
|
|
10
13
|
.archiveTask(taskId)
|
|
11
14
|
.then(taskId => {
|
|
12
15
|
console.log(`Archived task "${taskId}"`);
|
|
16
|
+
utility.showActionWarnings(kanbn);
|
|
13
17
|
})
|
|
14
18
|
.catch(error => {
|
|
15
19
|
utility.error(error);
|
|
@@ -32,11 +36,12 @@ function listArchivedTasks() {
|
|
|
32
36
|
|
|
33
37
|
module.exports = async args => {
|
|
34
38
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
// Work out which board this command targets
|
|
40
|
+
const scoped = await utility.resolveBoard(workspace, args);
|
|
41
|
+
if (scoped === null) {
|
|
38
42
|
return;
|
|
39
43
|
}
|
|
44
|
+
kanbn = scoped;
|
|
40
45
|
|
|
41
46
|
// Check if we're listing archived tasks
|
|
42
47
|
if (args.list) {
|
package/src/controller/board.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
const
|
|
1
|
+
const workspace = require('../main');
|
|
2
2
|
const utility = require('../utility');
|
|
3
3
|
const board = require('../board');
|
|
4
4
|
|
|
5
5
|
module.exports = async args => {
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
// Work out which board we're showing
|
|
8
|
+
const kanbn = await utility.resolveBoard(workspace, args);
|
|
9
|
+
if (kanbn === null) {
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -24,19 +24,27 @@ module.exports = async args => {
|
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// Load and hydrate all tracked tasks
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
// Load and hydrate all tracked tasks. A task the board references but which has no task file is
|
|
28
|
+
// skipped rather than fatal, and reported by the hint below
|
|
29
|
+
let tasks, missingTaskFiles;
|
|
30
|
+
try {
|
|
31
|
+
tasks = (await kanbn.loadAllTrackedTasks(index)).map(
|
|
32
|
+
task => kanbn.hydrateTask(index, task)
|
|
33
|
+
);
|
|
34
|
+
missingTaskFiles = await kanbn.findMissingTaskFiles(index);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
utility.error(error);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
31
39
|
|
|
32
40
|
// Show the board
|
|
33
41
|
board
|
|
34
42
|
.show(index, tasks, args.view, args.json)
|
|
35
|
-
.then(() => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
);
|
|
43
|
+
.then(async () => {
|
|
44
|
+
const verbose = !args.json && kanbn.isVerbose(index);
|
|
45
|
+
utility.showMissingTaskFilesHint(missingTaskFiles, verbose);
|
|
46
|
+
utility.showDateDriftHint(kanbn.getDateDrift(index, tasks), verbose);
|
|
47
|
+
utility.showBoardCountHint(await workspace.listBoards(), verbose);
|
|
40
48
|
})
|
|
41
49
|
.catch(error => {
|
|
42
50
|
utility.error(error);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const kanbn = require('../main');
|
|
2
|
+
const utility = require('../utility');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Show the list of boards in this workspace
|
|
6
|
+
* @param {object[]} boards
|
|
7
|
+
* @param {boolean} json
|
|
8
|
+
*/
|
|
9
|
+
function showBoards(boards, json) {
|
|
10
|
+
if (json) {
|
|
11
|
+
console.log(JSON.stringify(boards, null, 2));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
console.log(`${boards.length} ${boards.length === 1 ? 'board' : 'boards'}:`);
|
|
15
|
+
for (const board of boards) {
|
|
16
|
+
console.log(utility.replaceTags(`\n{b}${board.slug}{b}${board.main ? ' (main board)' : ''}`));
|
|
17
|
+
console.log(` ${board.name}`);
|
|
18
|
+
if (board.description) {
|
|
19
|
+
console.log(` ${board.description.split('\n')[0]}`);
|
|
20
|
+
}
|
|
21
|
+
console.log(
|
|
22
|
+
` ${board.columns} ${board.columns === 1 ? 'column' : 'columns'}, ` +
|
|
23
|
+
`${board.tasks} ${board.tasks === 1 ? 'task' : 'tasks'}, ` +
|
|
24
|
+
`${board.completedPercentage}% complete` +
|
|
25
|
+
(board.modified ? `, last modified ${board.modified.toISOString()}` : '')
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Show which boards each task appears on
|
|
32
|
+
* @param {object[]} tasks
|
|
33
|
+
* @param {boolean} all True if single-board tasks are included
|
|
34
|
+
* @param {boolean} json
|
|
35
|
+
*/
|
|
36
|
+
function showCrossBoardTasks(tasks, all, json) {
|
|
37
|
+
if (json) {
|
|
38
|
+
console.log(JSON.stringify(tasks, null, 2));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!tasks.length) {
|
|
42
|
+
console.log(all ? 'No tracked tasks found' : 'No tasks appear on more than one board');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
console.log(
|
|
46
|
+
`${tasks.length} ${tasks.length === 1 ? 'task' : 'tasks'}` +
|
|
47
|
+
(all ? ':' : ' on more than one board:')
|
|
48
|
+
);
|
|
49
|
+
for (const task of tasks) {
|
|
50
|
+
console.log(utility.replaceTags(`\n{b}${task.id}{b}`));
|
|
51
|
+
for (const [slug, columnName] of Object.entries(task.boards)) {
|
|
52
|
+
console.log(` ${slug}: ${columnName}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = async args => {
|
|
58
|
+
|
|
59
|
+
// Make sure kanbn has been initialised
|
|
60
|
+
if (!await kanbn.workspaceInitialised()) {
|
|
61
|
+
utility.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Delete a board
|
|
66
|
+
if (args.delete) {
|
|
67
|
+
const slug = utility.strArg(args.delete);
|
|
68
|
+
let orphaned;
|
|
69
|
+
try {
|
|
70
|
+
orphaned = await kanbn.findOrphanedTasks(slug);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
utility.error(error);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Deleting a board never deletes task files, but it can leave tasks with no board at all
|
|
77
|
+
if (orphaned.length && !args.force) {
|
|
78
|
+
utility.error(
|
|
79
|
+
`Deleting board "${slug}" would leave ${orphaned.length} ` +
|
|
80
|
+
`${orphaned.length === 1 ? 'task' : 'tasks'} untracked:\n` +
|
|
81
|
+
orphaned.map(taskId => ` ${taskId}`).join('\n') +
|
|
82
|
+
'\nUse {b}-f{b} to delete the board anyway'
|
|
83
|
+
);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await kanbn.deleteBoard(slug);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
utility.error(error);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
console.log(
|
|
93
|
+
`Deleted board "${slug}"` +
|
|
94
|
+
(orphaned.length
|
|
95
|
+
? `\n${orphaned.length} ${orphaned.length === 1 ? 'task is' : 'tasks are'} now untracked:\n` +
|
|
96
|
+
orphaned.map(taskId => ` ${taskId}`).join('\n')
|
|
97
|
+
: '')
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Rename a board, i.e. kanbn boards --rename design ux --name "UX Pipeline"
|
|
103
|
+
if (args.rename) {
|
|
104
|
+
const slug = utility.strArg(args.rename);
|
|
105
|
+
const newSlug = args._[1] || slug;
|
|
106
|
+
const newName = args.name ? utility.strArg(args.name) : null;
|
|
107
|
+
if (newSlug === slug && newName === null) {
|
|
108
|
+
utility.error('No new board slug or name specified\nTry running {b}kanbn boards --rename "board-slug" "new-slug"{b}');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const renamedSlug = await kanbn.renameBoard(slug, newSlug, newName);
|
|
113
|
+
console.log(
|
|
114
|
+
renamedSlug === slug
|
|
115
|
+
? `Renamed board "${slug}" to "${newName}"`
|
|
116
|
+
: `Renamed board "${slug}" to "${renamedSlug}"`
|
|
117
|
+
);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
utility.error(error);
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Show which boards each task appears on
|
|
125
|
+
if (args.tasks) {
|
|
126
|
+
try {
|
|
127
|
+
showCrossBoardTasks(await kanbn.getCrossBoardTasks(args.all), args.all, args.json);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
utility.error(error);
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// List boards
|
|
135
|
+
try {
|
|
136
|
+
showBoards(await kanbn.getBoardsSummary(), args.json);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
utility.error(error);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
const
|
|
1
|
+
const workspace = require('../main');
|
|
2
2
|
const utility = require('../utility');
|
|
3
3
|
const asciichart = require('asciichart');
|
|
4
4
|
const term = require('terminal-kit').terminal;
|
|
5
5
|
const chrono = require('chrono-node');
|
|
6
6
|
const formatDate = require('dateformat');
|
|
7
7
|
|
|
8
|
+
// The board this command targets - set from --board before anything else runs
|
|
9
|
+
let kanbn = workspace;
|
|
10
|
+
|
|
8
11
|
const getLabelPlacements = (from, to, width, dateFormat, maxLabels) => {
|
|
9
12
|
for (let count = maxLabels; count >= 1; count--) {
|
|
10
13
|
const placements = [];
|
|
@@ -62,11 +65,12 @@ const renderXAxisLabels = (from, to, width, dateFormat, leftPadding) => {
|
|
|
62
65
|
|
|
63
66
|
module.exports = async args => {
|
|
64
67
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
// Work out which board this command targets
|
|
69
|
+
const scoped = await utility.resolveBoard(workspace, args);
|
|
70
|
+
if (scoped === null) {
|
|
68
71
|
return;
|
|
69
72
|
}
|
|
73
|
+
kanbn = scoped;
|
|
70
74
|
const index = await kanbn.getIndex();
|
|
71
75
|
|
|
72
76
|
// Get sprint numbers or names
|
|
@@ -94,10 +98,13 @@ module.exports = async args => {
|
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
// Get assigned
|
|
101
|
+
// Get assigned, expanding "@me" to the current user
|
|
98
102
|
let assigned = null;
|
|
99
103
|
if (args.assigned) {
|
|
100
|
-
assigned = utility.strArg(args.assigned);
|
|
104
|
+
assigned = await utility.expandCurrentUser(kanbn, utility.strArg(args.assigned));
|
|
105
|
+
if (assigned === null) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
// Get columns
|
|
@@ -127,7 +134,7 @@ module.exports = async args => {
|
|
|
127
134
|
|
|
128
135
|
// Render chart
|
|
129
136
|
const PADDING = ' ';
|
|
130
|
-
const width = Math.max(1, term.width - (PADDING.length + 1));
|
|
137
|
+
const width = Math.max(1, utility.terminalWidth(term.width) - (PADDING.length + 1));
|
|
131
138
|
|
|
132
139
|
const plots = [];
|
|
133
140
|
for (const s of data.series) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
const
|
|
1
|
+
const workspace = require('../main');
|
|
2
2
|
const utility = require('../utility');
|
|
3
3
|
const inquirer = require('inquirer');
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
// The board this command targets - set from --board before anything else runs
|
|
6
|
+
let kanbn = workspace;
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Add a comment interactively
|
|
@@ -43,6 +45,7 @@ function addComment(taskId, text, author) {
|
|
|
43
45
|
.comment(taskId, text, author)
|
|
44
46
|
.then(taskId => {
|
|
45
47
|
console.log(`Added comment to task "${taskId}"`);
|
|
48
|
+
utility.showActionWarnings(kanbn);
|
|
46
49
|
})
|
|
47
50
|
.catch(error => {
|
|
48
51
|
utility.error(error);
|
|
@@ -51,11 +54,12 @@ function addComment(taskId, text, author) {
|
|
|
51
54
|
|
|
52
55
|
module.exports = async args => {
|
|
53
56
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
// Work out which board this command targets
|
|
58
|
+
const scoped = await utility.resolveBoard(workspace, args);
|
|
59
|
+
if (scoped === null) {
|
|
57
60
|
return;
|
|
58
61
|
}
|
|
62
|
+
kanbn = scoped;
|
|
59
63
|
|
|
60
64
|
// Get the task that we're add a comment to
|
|
61
65
|
const taskId = args._[1];
|
|
@@ -84,7 +88,10 @@ module.exports = async args => {
|
|
|
84
88
|
if (args.author && typeof args.author === 'string') {
|
|
85
89
|
commentAuthor = utility.strArg(args.author);
|
|
86
90
|
} else {
|
|
87
|
-
|
|
91
|
+
|
|
92
|
+
// Nobody at all is a legitimate answer on a machine with no git identity, and an empty author is
|
|
93
|
+
// what a comment without one looks like
|
|
94
|
+
commentAuthor = (await kanbn.currentUser()) || '';
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
// Add comment interactively
|