@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.
Files changed (100) hide show
  1. package/README.md +3 -1
  2. package/coverage/tmp/coverage-916017-1788028598821-0.json +1 -0
  3. package/coverage/tmp/{coverage-214293-1787777184569-0.json → coverage-916018-1788028597179-0.json} +1 -1
  4. package/coverage/tmp/coverage-916036-1788028598796-0.json +1 -0
  5. package/docs/actions.md +337 -0
  6. package/docs/advanced-configuration.md +55 -0
  7. package/docs/commands/add.txt +17 -1
  8. package/docs/commands/archive.txt +11 -0
  9. package/docs/commands/board.txt +9 -0
  10. package/docs/commands/boards.txt +34 -0
  11. package/docs/commands/burndown.txt +7 -0
  12. package/docs/commands/comment.txt +13 -1
  13. package/docs/commands/contributors.txt +58 -0
  14. package/docs/commands/edit.txt +18 -1
  15. package/docs/commands/find.txt +39 -1
  16. package/docs/commands/gantt.txt +6 -0
  17. package/docs/commands/help.txt +2 -0
  18. package/docs/commands/history.txt +6 -0
  19. package/docs/commands/init.txt +13 -0
  20. package/docs/commands/move.txt +26 -0
  21. package/docs/commands/remove.txt +22 -1
  22. package/docs/commands/rename.txt +5 -0
  23. package/docs/commands/restore.txt +12 -0
  24. package/docs/commands/sort.txt +23 -0
  25. package/docs/commands/sprint.txt +9 -0
  26. package/docs/commands/status.txt +10 -1
  27. package/docs/commands/task.txt +9 -0
  28. package/docs/commands/validate.txt +29 -1
  29. package/docs/contributors.md +145 -0
  30. package/docs/filtering-and-sorting.md +60 -3
  31. package/docs/index-structure.md +145 -11
  32. package/docs/index.md +6 -2
  33. package/docs/multiple-boards.md +259 -0
  34. package/docs/quick-start.md +21 -1
  35. package/docs/task-structure.md +31 -3
  36. package/example/README.md +23 -0
  37. package/example/advanced/kanbn.yml +65 -0
  38. package/example/boards/.kanbn/design.md +31 -0
  39. package/example/boards/.kanbn/index.md +44 -0
  40. package/example/boards/.kanbn/tasks/add-usage-alert-emails.md +19 -0
  41. package/example/boards/.kanbn/tasks/build-tenant-settings-page.md +43 -0
  42. package/example/boards/.kanbn/tasks/create-organization-switcher.md +44 -0
  43. package/example/boards/.kanbn/tasks/design-onboarding-checklist.md +22 -0
  44. package/example/boards/.kanbn/tasks/refresh-marketing-site.md +28 -0
  45. package/example/boards/.kanbn/tasks/ship-billing-portal.md +29 -0
  46. package/package.json +9 -7
  47. package/routes/add.json +40 -12
  48. package/routes/archive.json +14 -2
  49. package/routes/board.json +11 -3
  50. package/routes/boards.json +30 -0
  51. package/routes/burndown.json +23 -7
  52. package/routes/comment.json +19 -5
  53. package/routes/contributors.json +18 -0
  54. package/routes/edit.json +37 -11
  55. package/routes/find.json +37 -12
  56. package/routes/gantt.json +20 -6
  57. package/routes/history.json +39 -25
  58. package/routes/init.json +3 -1
  59. package/routes/move.json +24 -6
  60. package/routes/remove.json +18 -3
  61. package/routes/rename.json +11 -3
  62. package/routes/restore.json +14 -2
  63. package/routes/sort.json +40 -11
  64. package/routes/sprint.json +14 -4
  65. package/routes/status.json +23 -7
  66. package/routes/task.json +10 -2
  67. package/routes/validate.json +18 -5
  68. package/skills/kanbn-plan/SKILL.md +10 -1
  69. package/skills/kanbn-replan/SKILL.md +6 -1
  70. package/src/actions.js +904 -0
  71. package/src/board.js +25 -2
  72. package/src/controller/add.js +72 -55
  73. package/src/controller/archive.js +9 -4
  74. package/src/controller/board.js +21 -13
  75. package/src/controller/boards.js +140 -0
  76. package/src/controller/burndown.js +14 -7
  77. package/src/controller/comment.js +13 -6
  78. package/src/controller/contributors.js +166 -0
  79. package/src/controller/edit.js +48 -18
  80. package/src/controller/find.js +87 -13
  81. package/src/controller/gantt.js +13 -6
  82. package/src/controller/history.js +13 -6
  83. package/src/controller/init.js +39 -4
  84. package/src/controller/move.js +153 -25
  85. package/src/controller/remove.js +73 -13
  86. package/src/controller/rename.js +8 -4
  87. package/src/controller/restore.js +24 -8
  88. package/src/controller/sort.js +59 -3
  89. package/src/controller/sprint.js +31 -7
  90. package/src/controller/status.js +8 -4
  91. package/src/controller/task.js +34 -10
  92. package/src/controller/validate.js +197 -7
  93. package/src/git-user-name.js +9 -0
  94. package/src/git-user.js +55 -0
  95. package/src/main.d.ts +387 -9
  96. package/src/main.js +2757 -113
  97. package/src/parse-index.js +219 -17
  98. package/src/parse-task.js +16 -0
  99. package/src/utility.js +274 -0
  100. package/coverage/tmp/coverage-214292-1787777191526-0.json +0 -1
@@ -0,0 +1,166 @@
1
+ const workspace = require('../main');
2
+ const utility = require('../utility');
3
+
4
+ /**
5
+ * Show the workspace's contributors
6
+ * @param {object[]} contributors
7
+ * @param {?string} currentUser
8
+ * @param {boolean} json
9
+ */
10
+ function showContributors(contributors, currentUser, json) {
11
+ if (json) {
12
+ console.log(JSON.stringify({ contributors, currentUser }, null, 2));
13
+ return;
14
+ }
15
+ if (!contributors.length) {
16
+ console.log('No contributors are declared in this workspace');
17
+ console.log(utility.replaceTags(
18
+ `\n{d}Add a {d}{b}contributors{b}{d} list to your config file or the main board's front matter -{d} ` +
19
+ `{b}kanbn contributors --help{b}`
20
+ ));
21
+ showCurrentUser(currentUser);
22
+ return;
23
+ }
24
+ console.log(`${contributors.length} ${contributors.length === 1 ? 'contributor' : 'contributors'}:`);
25
+ for (const contributor of contributors) {
26
+ console.log(utility.replaceTags(
27
+ `\n{b}${contributor.name}{b}` +
28
+ (contributor.name === currentUser ? ' (you)' : '')
29
+ ));
30
+ if (contributor.displayName !== contributor.name) {
31
+ console.log(` ${contributor.displayName}`);
32
+ }
33
+ if (contributor.email) {
34
+ console.log(` ${contributor.email}`);
35
+ }
36
+ if (contributor.aliases.length) {
37
+ console.log(` also known as ${contributor.aliases.map(alias => `"${alias}"`).join(', ')}`);
38
+ }
39
+ if (contributor.colour) {
40
+ console.log(` ${contributor.colour}`);
41
+ }
42
+ }
43
+ showCurrentUser(currentUser, contributors);
44
+ }
45
+
46
+ /**
47
+ * Say who kanbn thinks the current user is, since that's what a bare --assigned and "@me" resolve to
48
+ * @param {?string} currentUser
49
+ * @param {object[]} [contributors=[]]
50
+ */
51
+ function showCurrentUser(currentUser, contributors = []) {
52
+ if (currentUser === null) {
53
+ console.log(utility.replaceTags(
54
+ `\n{d}Kanbn can't work out who you are. Set {d}{b}KANBN_USER{b}{d}, or set a git username with{d} ` +
55
+ `{b}git config user.name "your name"{b}`
56
+ ));
57
+ return;
58
+ }
59
+ if (contributors.length && !contributors.some(contributor => contributor.name === currentUser)) {
60
+ console.log(utility.replaceTags(
61
+ `\n{d}You are{d} {b}${currentUser}{b}{d}, which isn't in the contributors list{d}`
62
+ ));
63
+ return;
64
+ }
65
+ console.log(utility.replaceTags(`\n{d}You are{d} {b}${currentUser}{b}`));
66
+ }
67
+
68
+ /**
69
+ * Show how contributors are used across the workspace's tasks, and which names are in use that the
70
+ * contributors list doesn't know about
71
+ * @param {object} usage
72
+ * @param {?string} currentUser
73
+ * @param {boolean} json
74
+ */
75
+ function showUsage(usage, currentUser, json) {
76
+ if (json) {
77
+ console.log(JSON.stringify({ ...usage, currentUser }, null, 2));
78
+ return;
79
+ }
80
+ if (usage.contributors.length) {
81
+ console.log(`${usage.contributors.length} ${usage.contributors.length === 1 ? 'contributor' : 'contributors'}:`);
82
+ for (const contributor of usage.contributors) {
83
+ console.log(utility.replaceTags(
84
+ `\n{b}${contributor.name}{b}` +
85
+ (contributor.name === currentUser ? ' (you)' : '')
86
+ ));
87
+ console.log(
88
+ ` ${contributor.tasks} ${contributor.tasks === 1 ? 'task' : 'tasks'}, ` +
89
+ `assigned ${contributor.assigned}, ` +
90
+ `${contributor.comments} ${contributor.comments === 1 ? 'comment' : 'comments'}`
91
+ );
92
+
93
+ // A contributor matched through an alias or a different case is worth showing: it's a spelling
94
+ // that works today only because the alias is there to catch it
95
+ for (const spelling of contributor.spellings.filter(s => s.value !== contributor.name)) {
96
+ console.log(` written as "${spelling.value}" in ${spelling.tasks.length} ${spelling.tasks.length === 1 ? 'task' : 'tasks'}`);
97
+ }
98
+ }
99
+ } else {
100
+ console.log('No contributors are declared in this workspace');
101
+ }
102
+
103
+ if (!usage.unknown.length) {
104
+ console.log(
105
+ usage.contributors.length
106
+ ? '\nEvery name used in a task matches a contributor'
107
+ : '\nNo names are used in any task'
108
+ );
109
+ return;
110
+ }
111
+ console.log(
112
+ `\n${usage.unknown.length} ${usage.unknown.length === 1 ? 'name is' : 'names are'} used in tasks ` +
113
+ `but ${usage.unknown.length === 1 ? "isn't" : "aren't"} a known contributor:`
114
+ );
115
+ for (const unknown of usage.unknown) {
116
+ console.log(utility.replaceTags(
117
+ `\n{b}${unknown.value}{b}` +
118
+ (unknown.value === currentUser ? ' (you)' : '')
119
+ ));
120
+ console.log(
121
+ ` ${unknown.tasks.length} ${unknown.tasks.length === 1 ? 'task' : 'tasks'}, ` +
122
+ `assigned ${unknown.assigned}, ` +
123
+ `${unknown.comments} ${unknown.comments === 1 ? 'comment' : 'comments'}`
124
+ );
125
+ console.log(` ${unknown.tasks.join(', ')}`);
126
+ }
127
+ console.log(utility.replaceTags(
128
+ `\n{d}Add these to {d}{b}contributors{b}{d}, or as an {d}{b}alias{b}{d} of a contributor already ` +
129
+ `in the list. Nothing is rewritten either way - {d}{b}assigned{b}{d} and comment {d}{b}author{b}` +
130
+ `{d} are free text{d}`
131
+ ));
132
+ }
133
+
134
+ module.exports = async args => {
135
+
136
+ // Contributors are workspace-scoped, so this command isn't board-scoped either
137
+ if (!await workspace.workspaceInitialised()) {
138
+ utility.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
139
+ return;
140
+ }
141
+
142
+ let currentUser;
143
+ try {
144
+ currentUser = await workspace.currentUser();
145
+ } catch (error) {
146
+ utility.error(error);
147
+ return;
148
+ }
149
+
150
+ // Show how the contributors are actually used across the workspace's tasks
151
+ if (args.usage) {
152
+ try {
153
+ showUsage(await workspace.getContributorUsage(), currentUser, args.json);
154
+ } catch (error) {
155
+ utility.error(error);
156
+ }
157
+ return;
158
+ }
159
+
160
+ // List contributors
161
+ try {
162
+ showContributors(await workspace.getContributors(), currentUser, args.json);
163
+ } catch (error) {
164
+ utility.error(error);
165
+ }
166
+ };
@@ -1,9 +1,11 @@
1
- const kanbn = require('../main');
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
- const getGitUsername = require('git-user-name');
6
+
7
+ // The board this command targets - set from --board before anything else runs
8
+ let kanbn = workspace;
7
9
 
8
10
  inquirer.registerPrompt('datepicker', require('inquirer-datepicker'));
9
11
  inquirer.registerPrompt('recursive', require('inquirer-recursive'));
@@ -15,9 +17,11 @@ inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt'))
15
17
  * @param {string[]} taskIds
16
18
  * @param {string} columnName
17
19
  * @param {string[]} columnNames
20
+ * @param {?string} currentUser
21
+ * @param {object[]} contributors
18
22
  * @return {Promise<any>}
19
23
  */
20
- async function interactive(taskData, taskIds, columnName, columnNames) {
24
+ async function interactive(taskData, taskIds, columnName, columnNames, currentUser, contributors) {
21
25
  const dueDateExists = (
22
26
  'metadata' in taskData &&
23
27
  'due' in taskData.metadata &&
@@ -223,13 +227,12 @@ async function interactive(taskData, taskIds, columnName, columnNames) {
223
227
  default: false,
224
228
  when: answers => !assignedExists
225
229
  },
226
- {
227
- type: 'input',
230
+ utility.assignedPrompt({
228
231
  name: 'assigned',
229
232
  message: 'Assigned to:',
230
- default: assignedExists ? taskData.metadata.assigned : getGitUsername(),
233
+ default: assignedExists ? taskData.metadata.assigned : (currentUser || ''),
231
234
  when: answers => answers.setAssigned || answers.editAssigned === 'edit'
232
- },
235
+ }, contributors),
233
236
  {
234
237
  type: 'recursive',
235
238
  name: 'addSubTasks',
@@ -447,6 +450,7 @@ function updateTask(taskId, taskData, columnName, unsetFields = []) {
447
450
  .updateTask(taskId, taskData, columnName, unsetFields)
448
451
  .then(taskId => {
449
452
  console.log(`Updated task "${taskId}"`);
453
+ utility.showActionWarnings(kanbn);
450
454
  })
451
455
  .catch(error => {
452
456
  utility.error(error);
@@ -455,25 +459,42 @@ function updateTask(taskId, taskData, columnName, unsetFields = []) {
455
459
 
456
460
  module.exports = async args => {
457
461
 
458
- // Make sure kanbn has been initialised
459
- if (!await kanbn.initialised()) {
460
- utility.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
462
+ // Work out which board this command targets
463
+ const scoped = await utility.resolveBoard(workspace, args);
464
+ if (scoped === null) {
461
465
  return;
462
466
  }
467
+ kanbn = scoped;
463
468
 
464
469
  // Get the task that we're editing
465
- const taskId = args._[1];
470
+ let taskId = args._[1];
466
471
  if (!taskId) {
467
472
  utility.error('No task id specified\nTry running {b}kanbn edit "task id"{b}');
468
473
  return;
469
474
  }
470
475
 
471
- // Make sure the task exists
476
+ // Make sure the task exists. Editing a simple task is how it becomes a real one - there is nothing
477
+ // to edit until it has a file - so promote it first and then edit the task that produces
472
478
  try {
473
479
  await kanbn.taskExists(taskId);
474
480
  } catch (error) {
475
- utility.error(error);
476
- return;
481
+ if (await kanbn.taskFileExists(taskId)) {
482
+ utility.error(error);
483
+ return;
484
+ }
485
+ const simpleTask = await utility.resolveSimpleTask(kanbn, taskId, error);
486
+ if (simpleTask === null) {
487
+ return;
488
+ }
489
+ try {
490
+ taskId = await kanbn.promoteSimpleTask(taskId);
491
+ console.log(utility.replaceTags(
492
+ `{d}Promoted simple task "${simpleTask.text}" to task "${taskId}"{d}`
493
+ ));
494
+ } catch (promoteError) {
495
+ utility.error(promoteError);
496
+ return;
497
+ }
477
498
  }
478
499
 
479
500
  // Get the index and make sure it has some columns
@@ -578,10 +599,12 @@ module.exports = async args => {
578
599
  if (!('metadata' in taskData)) {
579
600
  taskData.metadata = {};
580
601
  }
581
- const gitUsername = getGitUsername();
582
602
  if (args.assigned === true) {
583
- if (gitUsername) {
584
- taskData.metadata.assigned = gitUsername;
603
+
604
+ // A bare --assigned means "me", canonicalised against the contributors list if there is one
605
+ const currentUser = await kanbn.currentUser();
606
+ if (currentUser) {
607
+ taskData.metadata.assigned = currentUser;
585
608
  }
586
609
  } else {
587
610
  taskData.metadata.assigned = utility.strArg(args.assigned);
@@ -809,7 +832,14 @@ module.exports = async args => {
809
832
 
810
833
  // Update task interactively
811
834
  if (args.interactive) {
812
- interactive(taskData, taskIds, columnName, columnNames)
835
+ interactive(
836
+ taskData,
837
+ taskIds,
838
+ columnName,
839
+ columnNames,
840
+ await kanbn.currentUser(),
841
+ await kanbn.getContributors()
842
+ )
813
843
  .then(answers => {
814
844
 
815
845
  // Name
@@ -1,9 +1,12 @@
1
- const kanbn = require('../main');
1
+ const workspace = require('../main');
2
2
  const utility = require('../utility');
3
3
  const inquirer = require('inquirer');
4
4
  const chrono = require('chrono-node');
5
5
  const yaml = require('yamljs');
6
6
 
7
+ // The board this command targets - set from --board before anything else runs
8
+ let kanbn = workspace;
9
+
7
10
  inquirer.registerPrompt('recursive', require('inquirer-recursive'));
8
11
 
9
12
  const searchFields = [
@@ -111,6 +114,31 @@ const searchFields = [
111
114
  name: 'Assigned',
112
115
  field: 'assigned',
113
116
  type: 'string'
117
+ },
118
+ {
119
+ name: 'Overdue',
120
+ field: 'overdue',
121
+ type: 'boolean'
122
+ },
123
+ {
124
+ name: 'Is started',
125
+ field: 'is-started',
126
+ type: 'boolean'
127
+ },
128
+ {
129
+ name: 'Is completed',
130
+ field: 'is-completed',
131
+ type: 'boolean'
132
+ },
133
+ {
134
+ name: 'In started column',
135
+ field: 'in-started-column',
136
+ type: 'boolean'
137
+ },
138
+ {
139
+ name: 'In completed column',
140
+ field: 'in-completed-column',
141
+ type: 'boolean'
114
142
  }
115
143
  ];
116
144
 
@@ -213,12 +241,11 @@ async function interactive() {
213
241
  * @param {boolean} quiet
214
242
  * @param {boolean} json
215
243
  */
216
- function findTasks(filters, quiet, json) {
244
+ function findTasks(filters, quiet, json, allBoards = false) {
217
245
  const removeEmptyProperties = o => Object.fromEntries(Object.entries(o).filter(
218
246
  ([k, v]) => !(Array.isArray(v) && v.length == 0) && !!v
219
247
  ));
220
- kanbn
221
- .search(filters, quiet)
248
+ (allBoards ? kanbn.searchAllBoards(filters, quiet) : kanbn.search(filters, quiet))
222
249
  .then(results => {
223
250
  if (quiet) {
224
251
  console.log(json ? JSON.stringify(results, null, 2) : results.join('\n'));
@@ -282,6 +309,24 @@ function convertNumericFilters(filters, filterName) {
282
309
  return true;
283
310
  }
284
311
 
312
+ /**
313
+ * Check that a filter holds a boolean value. Repeating a boolean option is meaningless, so the last
314
+ * value wins
315
+ * @param {object} filters The current filter object
316
+ * @param {string} filterName The property name for the filter
317
+ * @return {boolean} True if the value is a boolean
318
+ */
319
+ function convertBooleanFilters(filters, filterName) {
320
+ const value = Array.isArray(filters[filterName])
321
+ ? filters[filterName][filters[filterName].length - 1]
322
+ : filters[filterName];
323
+ if (typeof value !== 'boolean') {
324
+ return false;
325
+ }
326
+ filters[filterName] = value;
327
+ return true;
328
+ }
329
+
285
330
  /**
286
331
  * Convert a filter or array of filters to date values
287
332
  * @param {object} filters The current filter object
@@ -309,11 +354,12 @@ function convertDateFilters(filters, filterName) {
309
354
 
310
355
  module.exports = async args => {
311
356
 
312
- // Make sure kanbn has been initialised
313
- if (!await kanbn.initialised()) {
314
- utility.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
357
+ // Work out which board this command targets
358
+ const scoped = await utility.resolveBoard(workspace, args);
359
+ if (scoped === null) {
315
360
  return;
316
361
  }
362
+ kanbn = scoped;
317
363
 
318
364
  // Get the index and make sure it has some columns
319
365
  let index;
@@ -340,12 +386,26 @@ module.exports = async args => {
340
386
  }
341
387
  }
342
388
 
343
- // Get filters from args
389
+ // Get filters from args. Boolean filters are checked with "in" because false is a filter value in
390
+ // its own right - "--no-overdue" means "not overdue", not "no overdue filter"
344
391
  const filters = {};
345
- for (let filterProperty of searchFields.map(s => s.field)) {
346
- if (args[filterProperty]) {
347
- filters[filterProperty] = args[filterProperty];
392
+ for (let searchField of searchFields) {
393
+ if (searchField.type === 'boolean') {
394
+ if (searchField.field in args) {
395
+ filters[searchField.field] = args[searchField.field];
396
+ }
397
+ } else if (args[searchField.field]) {
398
+ filters[searchField.field] = args[searchField.field];
399
+ }
400
+ }
401
+
402
+ // Expand "@me" in the assigned filter
403
+ if ('assigned' in filters) {
404
+ const assigned = await utility.expandCurrentUser(kanbn, filters.assigned);
405
+ if (assigned === null) {
406
+ return;
348
407
  }
408
+ filters.assigned = assigned;
349
409
  }
350
410
 
351
411
  // Check and convert numeric filters
@@ -386,6 +446,20 @@ module.exports = async args => {
386
446
  }
387
447
  }
388
448
 
449
+ // Check computed boolean filters
450
+ for (const [filterName, label] of [
451
+ ['overdue', 'Overdue'],
452
+ ['is-started', 'Is started'],
453
+ ['is-completed', 'Is completed'],
454
+ ['in-started-column', 'In started column'],
455
+ ['in-completed-column', 'In completed column']
456
+ ]) {
457
+ if (filterName in filters && !convertBooleanFilters(filters, filterName)) {
458
+ utility.error(`${label} filter value must be a boolean`);
459
+ return;
460
+ }
461
+ }
462
+
389
463
  // Check date filters
390
464
  if ('created' in filters) {
391
465
  if (!convertDateFilters(filters, 'created')) {
@@ -487,7 +561,7 @@ module.exports = async args => {
487
561
  filter.value
488
562
  );
489
563
  }
490
- findTasks(filters, !otherAnswers.nonquiet, otherAnswers.json);
564
+ findTasks(filters, !otherAnswers.nonquiet, otherAnswers.json, !!args['all-boards']);
491
565
  })
492
566
  .catch(error => {
493
567
  utility.error(error);
@@ -499,6 +573,6 @@ module.exports = async args => {
499
573
 
500
574
  // Otherwise create task non-interactively
501
575
  } else {
502
- findTasks(filters, args.quiet, args.json);
576
+ findTasks(filters, args.quiet, args.json, !!args['all-boards']);
503
577
  }
504
578
  };
@@ -1,8 +1,11 @@
1
- const kanbn = require('../main');
1
+ const workspace = require('../main');
2
2
  const utility = require('../utility');
3
3
  const formatDate = require('dateformat');
4
4
  const chrono = require('chrono-node');
5
5
 
6
+ // The board this command targets - set from --board before anything else runs
7
+ let kanbn = workspace;
8
+
6
9
  const NOW_MARKER = '┆';
7
10
  const ANSI_RESET = '\x1b[0m';
8
11
  const ANSI_FG_LINE = '\x1b[97m';
@@ -313,17 +316,21 @@ function truncateName(name, maxWidth) {
313
316
 
314
317
  module.exports = async args => {
315
318
 
316
- // Make sure kanbn has been initialised
317
- if (!await kanbn.initialised()) {
318
- utility.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
319
+ // Work out which board this command targets
320
+ const scoped = await utility.resolveBoard(workspace, args);
321
+ if (scoped === null) {
319
322
  return;
320
323
  }
324
+ kanbn = scoped;
321
325
  const index = await kanbn.getIndex();
322
326
 
323
- // Get assigned
327
+ // Get assigned, expanding "@me" to the current user
324
328
  let assigned = null;
325
329
  if (args.assigned) {
326
- assigned = utility.strArg(args.assigned);
330
+ assigned = await utility.expandCurrentUser(kanbn, utility.strArg(args.assigned));
331
+ if (assigned === null) {
332
+ return;
333
+ }
327
334
  }
328
335
 
329
336
  // Get columns
@@ -1,8 +1,11 @@
1
- const kanbn = require('../main');
1
+ const workspace = require('../main');
2
2
  const utility = require('../utility');
3
3
  const chrono = require('chrono-node');
4
4
  const formatDate = require('dateformat');
5
5
 
6
+ // The board this command targets - set from --board before anything else runs
7
+ let kanbn = workspace;
8
+
6
9
  function formatHistoryEvent(event) {
7
10
  const details = [];
8
11
  if ('column' in event.event) {
@@ -29,11 +32,12 @@ function formatMetadataEvent(event, dateFormat) {
29
32
 
30
33
  module.exports = async args => {
31
34
 
32
- // Make sure kanbn has been initialised
33
- if (!await kanbn.initialised()) {
34
- utility.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
35
+ // Work out which board this command targets
36
+ const scoped = await utility.resolveBoard(workspace, args);
37
+ if (scoped === null) {
35
38
  return;
36
39
  }
40
+ kanbn = scoped;
37
41
  const index = await kanbn.getIndex();
38
42
 
39
43
  // Get sprint numbers or names
@@ -61,10 +65,13 @@ module.exports = async args => {
61
65
  }
62
66
  }
63
67
 
64
- // Get assigned
68
+ // Get assigned, expanding "@me" to the current user
65
69
  let assigned = null;
66
70
  if (args.assigned) {
67
- assigned = utility.strArg(args.assigned);
71
+ assigned = await utility.expandCurrentUser(kanbn, utility.strArg(args.assigned));
72
+ if (assigned === null) {
73
+ return;
74
+ }
68
75
  }
69
76
 
70
77
  // Get task ids
@@ -1,8 +1,11 @@
1
1
  const kanbn_module = require('../main');
2
- const kanbn = new kanbn_module.Kanbn();
2
+ const workspace = new kanbn_module.Kanbn();
3
3
  const utility = require('../utility');
4
4
  const inquirer = require('inquirer');
5
5
 
6
+ // The board being initialised - set from --board before anything else runs
7
+ let kanbn = workspace;
8
+
6
9
  inquirer.registerPrompt('recursive', require('inquirer-recursive'));
7
10
 
8
11
  /**
@@ -73,11 +76,20 @@ async function interactive(options, initialised) {
73
76
  * Initialise kanbn
74
77
  * @param {object} options
75
78
  * @param {boolean} initialised
79
+ * @param {?string} boardSlug The slug of the board being initialised, or null for the main board
76
80
  */
77
- async function initialise(options, initialised) {
81
+ async function initialise(options, initialised, boardSlug = null) {
78
82
  const mainFolder = await kanbn.getMainFolder();
79
83
  kanbn.initialise(options)
80
84
  .then(() => {
85
+ if (boardSlug !== null) {
86
+ console.log(
87
+ initialised
88
+ ? `Updated board "${boardSlug}" in ${mainFolder}`
89
+ : `Created board "${boardSlug}" in ${mainFolder}`
90
+ );
91
+ return;
92
+ }
81
93
  if (initialised) {
82
94
  console.log(`Reinitialised existing kanbn board in ${mainFolder}`);
83
95
  } else {
@@ -92,6 +104,29 @@ async function initialise(options, initialised) {
92
104
  module.exports = async args => {
93
105
  let options = {};
94
106
 
107
+ // Work out which board we're initialising. Without --board this is the main board, i.e. the whole
108
+ // workspace, which is exactly what init has always done
109
+ let boardSlug = null;
110
+ if (args.board) {
111
+ boardSlug = utility.strArg(args.board);
112
+
113
+ // A secondary board lives beside the main board and shares its tasks, so the workspace has to
114
+ // exist first
115
+ if (!await workspace.initialised()) {
116
+ utility.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
117
+ return;
118
+ }
119
+ try {
120
+ boardSlug = await workspace.validateBoardSlug(boardSlug);
121
+ } catch (error) {
122
+ utility.error(error);
123
+ return;
124
+ }
125
+ kanbn = workspace.board(boardSlug);
126
+ } else {
127
+ kanbn = workspace;
128
+ }
129
+
95
130
  // If this folder is already initialised, set the default name and description using the current values
96
131
  const initialised = await kanbn.initialised();
97
132
  if (initialised) {
@@ -129,7 +164,7 @@ module.exports = async args => {
129
164
  if ('columns' in answers) {
130
165
  answers.columns = answers.columns.map(column => column.columnName);
131
166
  }
132
- await initialise(answers, initialised);
167
+ await initialise(answers, initialised, boardSlug);
133
168
  })
134
169
  .catch(error => {
135
170
  utility.error(error);
@@ -137,6 +172,6 @@ module.exports = async args => {
137
172
 
138
173
  // Non-interactive initialisation
139
174
  } else {
140
- await initialise(options, initialised);
175
+ await initialise(options, initialised, boardSlug);
141
176
  }
142
177
  };