@basementuniverse/kanbn 1.1.1 → 2.1.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 (182) hide show
  1. package/README.md +3 -0
  2. package/coverage/base.css +224 -0
  3. package/coverage/block-navigation.js +87 -0
  4. package/coverage/coverage-summary.json +31 -0
  5. package/coverage/favicon.png +0 -0
  6. package/coverage/index.html +146 -0
  7. package/coverage/kanbn/index.html +116 -0
  8. package/coverage/kanbn/index.js.html +217 -0
  9. package/coverage/kanbn/src/board.js.html +646 -0
  10. package/coverage/kanbn/src/controller/add.js.html +1729 -0
  11. package/coverage/kanbn/src/controller/archive.js.html +277 -0
  12. package/coverage/kanbn/src/controller/board.js.html +217 -0
  13. package/coverage/kanbn/src/controller/burndown.js.html +607 -0
  14. package/coverage/kanbn/src/controller/comment.js.html +397 -0
  15. package/coverage/kanbn/src/controller/edit.js.html +2920 -0
  16. package/coverage/kanbn/src/controller/find.js.html +1597 -0
  17. package/coverage/kanbn/src/controller/gantt.js.html +1342 -0
  18. package/coverage/kanbn/src/controller/help.js.html +115 -0
  19. package/coverage/kanbn/src/controller/history.js.html +400 -0
  20. package/coverage/kanbn/src/controller/index.html +431 -0
  21. package/coverage/kanbn/src/controller/init.js.html +511 -0
  22. package/coverage/kanbn/src/controller/move.js.html +514 -0
  23. package/coverage/kanbn/src/controller/remove-all.js.html +226 -0
  24. package/coverage/kanbn/src/controller/remove.js.html +307 -0
  25. package/coverage/kanbn/src/controller/rename.js.html +397 -0
  26. package/coverage/kanbn/src/controller/restore.js.html +268 -0
  27. package/coverage/kanbn/src/controller/sort.js.html +1318 -0
  28. package/coverage/kanbn/src/controller/sprint.js.html +361 -0
  29. package/coverage/kanbn/src/controller/status.js.html +310 -0
  30. package/coverage/kanbn/src/controller/task.js.html +244 -0
  31. package/coverage/kanbn/src/controller/validate.js.html +367 -0
  32. package/coverage/kanbn/src/controller/version.js.html +100 -0
  33. package/coverage/kanbn/src/index.html +191 -0
  34. package/coverage/kanbn/src/main.js.html +9787 -0
  35. package/coverage/kanbn/src/parse-index.js.html +1063 -0
  36. package/coverage/kanbn/src/parse-markdown.js.html +202 -0
  37. package/coverage/kanbn/src/parse-task.js.html +2191 -0
  38. package/coverage/kanbn/src/utility.js.html +616 -0
  39. package/coverage/lcov.info +11837 -0
  40. package/coverage/prettify.css +1 -0
  41. package/coverage/prettify.js +2 -0
  42. package/coverage/sort-arrow-sprite.png +0 -0
  43. package/coverage/sorter.js +210 -0
  44. package/coverage/tmp/coverage-214292-1787777191526-0.json +1 -0
  45. package/coverage/tmp/coverage-214293-1787777184569-0.json +1 -0
  46. package/docs/advanced-configuration.md +25 -0
  47. package/docs/commands/add.txt +19 -0
  48. package/docs/commands/archive.txt +5 -0
  49. package/docs/commands/board.txt +8 -1
  50. package/docs/commands/boards.txt +34 -0
  51. package/docs/commands/burndown.txt +8 -4
  52. package/docs/commands/comment.txt +5 -0
  53. package/docs/commands/edit.txt +27 -0
  54. package/docs/commands/find.txt +56 -1
  55. package/docs/commands/gantt.txt +5 -0
  56. package/docs/commands/help.txt +2 -0
  57. package/docs/commands/history.txt +5 -0
  58. package/docs/commands/init.txt +13 -0
  59. package/docs/commands/move.txt +10 -1
  60. package/docs/commands/remove.txt +12 -1
  61. package/docs/commands/rename.txt +5 -0
  62. package/docs/commands/restore.txt +6 -0
  63. package/docs/commands/sort.txt +18 -2
  64. package/docs/commands/sprint.txt +9 -0
  65. package/docs/commands/status.txt +10 -1
  66. package/docs/commands/task.txt +5 -0
  67. package/docs/commands/validate.txt +27 -0
  68. package/docs/filtering-and-sorting.md +218 -0
  69. package/docs/index-structure.md +94 -19
  70. package/docs/index.md +28 -2
  71. package/docs/migration-2.0.md +92 -0
  72. package/docs/multiple-boards.md +258 -0
  73. package/docs/quick-start.md +57 -1
  74. package/docs/task-structure.md +33 -3
  75. package/docs/views.md +229 -0
  76. package/example/README.md +93 -0
  77. package/example/advanced/.kanbn/index.md +26 -0
  78. package/example/advanced/.kanbn/tasks/add-rate-limiting.md +18 -0
  79. package/example/advanced/.kanbn/tasks/add-sso-support.md +15 -0
  80. package/example/advanced/.kanbn/tasks/migrate-to-new-cdn.md +14 -0
  81. package/example/advanced/.kanbn/tasks/refresh-marketing-site.md +14 -0
  82. package/example/advanced/.kanbn/tasks/rotate-api-keys.md +15 -0
  83. package/example/advanced/.kanbn/tasks/split-billing-service.md +17 -0
  84. package/example/advanced/.kanbn/tasks/upgrade-node-runtime.md +19 -0
  85. package/example/advanced/kanbn.yml +59 -0
  86. package/example/boards/.kanbn/design.md +31 -0
  87. package/example/boards/.kanbn/index.md +44 -0
  88. package/example/boards/.kanbn/tasks/add-usage-alert-emails.md +19 -0
  89. package/example/boards/.kanbn/tasks/build-tenant-settings-page.md +43 -0
  90. package/example/boards/.kanbn/tasks/create-organization-switcher.md +44 -0
  91. package/example/boards/.kanbn/tasks/design-onboarding-checklist.md +22 -0
  92. package/example/boards/.kanbn/tasks/refresh-marketing-site.md +28 -0
  93. package/example/boards/.kanbn/tasks/ship-billing-portal.md +29 -0
  94. package/example/views/.kanbn/index.md +140 -0
  95. package/example/views/.kanbn/tasks/add-audit-log-export.md +18 -0
  96. package/example/views/.kanbn/tasks/add-dark-mode-toggle.md +16 -0
  97. package/example/views/.kanbn/tasks/build-webhook-retry-queue.md +15 -0
  98. package/example/views/.kanbn/tasks/fix-avatar-upload-crash.md +13 -0
  99. package/example/views/.kanbn/tasks/fix-expired-session-redirect.md +15 -0
  100. package/example/views/.kanbn/tasks/investigate-offline-mode.md +12 -0
  101. package/example/views/.kanbn/tasks/rewrite-search-indexer.md +12 -0
  102. package/example/views/.kanbn/tasks/tidy-up-settings-copy.md +12 -0
  103. package/package.json +12 -9
  104. package/routes/add.json +35 -11
  105. package/routes/archive.json +10 -2
  106. package/routes/board.json +11 -3
  107. package/routes/boards.json +30 -0
  108. package/routes/burndown.json +23 -7
  109. package/routes/comment.json +14 -4
  110. package/routes/edit.json +33 -10
  111. package/routes/find.json +37 -12
  112. package/routes/gantt.json +20 -6
  113. package/routes/history.json +39 -25
  114. package/routes/init.json +3 -1
  115. package/routes/move.json +22 -6
  116. package/routes/remove.json +15 -4
  117. package/routes/rename.json +11 -3
  118. package/routes/restore.json +8 -2
  119. package/routes/sort.json +35 -11
  120. package/routes/sprint.json +14 -4
  121. package/routes/status.json +23 -7
  122. package/routes/task.json +10 -2
  123. package/routes/validate.json +19 -4
  124. package/skills/kanbn-plan/SKILL.md +10 -1
  125. package/skills/kanbn-plan/references/index-structure.md +1 -0
  126. package/skills/kanbn-plan/references/task-structure.md +2 -0
  127. package/skills/kanbn-replan/SKILL.md +6 -1
  128. package/src/board.js +2 -1
  129. package/src/controller/add.js +52 -46
  130. package/src/controller/archive.js +8 -4
  131. package/src/controller/board.js +9 -4
  132. package/src/controller/boards.js +140 -0
  133. package/src/controller/burndown.js +9 -5
  134. package/src/controller/comment.js +9 -5
  135. package/src/controller/edit.js +74 -17
  136. package/src/controller/find.js +12 -9
  137. package/src/controller/gantt.js +8 -4
  138. package/src/controller/history.js +8 -4
  139. package/src/controller/init.js +39 -4
  140. package/src/controller/move.js +69 -15
  141. package/src/controller/remove.js +34 -11
  142. package/src/controller/rename.js +8 -4
  143. package/src/controller/restore.js +23 -8
  144. package/src/controller/sort.js +19 -3
  145. package/src/controller/sprint.js +31 -7
  146. package/src/controller/status.js +20 -4
  147. package/src/controller/task.js +22 -9
  148. package/src/controller/validate.js +139 -23
  149. package/src/git-user-name.js +19 -0
  150. package/src/main.d.ts +184 -6
  151. package/src/main.js +1642 -221
  152. package/src/parse-index.js +17 -0
  153. package/src/parse-markdown.js +3 -1
  154. package/src/parse-task.js +44 -1
  155. package/src/utility.js +166 -1
  156. package/PLANS.md +0 -9
  157. /package/example/{.kanbn → basic/.kanbn}/index.md +0 -0
  158. /package/example/{.kanbn → basic/.kanbn}/tasks/add-basic-activity-feed.md +0 -0
  159. /package/example/{.kanbn → basic/.kanbn}/tasks/add-passwordless-login-option.md +0 -0
  160. /package/example/{.kanbn → basic/.kanbn}/tasks/add-usage-alert-email-thresholds.md +0 -0
  161. /package/example/{.kanbn → basic/.kanbn}/tasks/build-email-template-system.md +0 -0
  162. /package/example/{.kanbn → basic/.kanbn}/tasks/build-invoice-download-endpoint.md +0 -0
  163. /package/example/{.kanbn → basic/.kanbn}/tasks/build-tenant-settings-page.md +0 -0
  164. /package/example/{.kanbn → basic/.kanbn}/tasks/create-organization-switcher.md +0 -0
  165. /package/example/{.kanbn → basic/.kanbn}/tasks/create-sandbox-environment-provisioner.md +0 -0
  166. /package/example/{.kanbn → basic/.kanbn}/tasks/create-self-serve-cancellation-flow.md +0 -0
  167. /package/example/{.kanbn → basic/.kanbn}/tasks/define-product-pricing-strategy.md +0 -0
  168. /package/example/{.kanbn → basic/.kanbn}/tasks/design-onboarding-checklist.md +0 -0
  169. /package/example/{.kanbn → basic/.kanbn}/tasks/design-team-invite-expiry-flow.md +0 -0
  170. /package/example/{.kanbn → basic/.kanbn}/tasks/implement-data-retention-policy-jobs.md +0 -0
  171. /package/example/{.kanbn → basic/.kanbn}/tasks/implement-feature-flags-foundation.md +0 -0
  172. /package/example/{.kanbn → basic/.kanbn}/tasks/implement-project-creation-wizard.md +0 -0
  173. /package/example/{.kanbn → basic/.kanbn}/tasks/implement-stripe-webhook-signature-check.md +0 -0
  174. /package/example/{.kanbn → basic/.kanbn}/tasks/implement-team-permissions-ui.md +0 -0
  175. /package/example/{.kanbn → basic/.kanbn}/tasks/implement-user-signup-and-login.md +0 -0
  176. /package/example/{.kanbn → basic/.kanbn}/tasks/integrate-crm-lead-sync.md +0 -0
  177. /package/example/{.kanbn → basic/.kanbn}/tasks/legal-review-terms-and-privacy.md +0 -0
  178. /package/example/{.kanbn → basic/.kanbn}/tasks/migrate-legacy-events-to-new-schema.md +0 -0
  179. /package/example/{.kanbn → basic/.kanbn}/tasks/optimize-dashboard-first-load.md +0 -0
  180. /package/example/{.kanbn → basic/.kanbn}/tasks/prototype-report-export-scheduler.md +0 -0
  181. /package/example/{.kanbn → basic/.kanbn}/tasks/publish-internal-qa-checklist.md +0 -0
  182. /package/example/{.kanbn → basic/.kanbn}/tasks/setup-ci-pipeline.md +0 -0
@@ -1,9 +1,13 @@
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
+ const getGitUsername = require('../git-user-name');
7
+
8
+ // The boards this command targets - set from --board before anything else runs
9
+ let kanbn = workspace;
10
+ let boardTargets = [];
7
11
 
8
12
  inquirer.registerPrompt('datepicker', require('inquirer-datepicker'));
9
13
  inquirer.registerPrompt('recursive', require('inquirer-recursive'));
@@ -235,15 +239,24 @@ async function interactiveAddUntrackedTasks(untrackedTasks, columnName, columnNa
235
239
  * @param {object} taskData
236
240
  * @param {string} columnName
237
241
  */
238
- function createTask(taskData, columnName) {
239
- kanbn
240
- .createTask(taskData, columnName)
241
- .then(taskId => {
242
- console.log(`Created task "${taskId}" in column "${columnName}"`);
243
- })
244
- .catch(error => {
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
+ for (const target of rest) {
254
+ await target.kanbn.addTaskToBoard(taskId, target.columnName);
255
+ console.log(`Added task "${taskId}" to column "${target.columnName}" on board "${target.slug}"`);
256
+ }
257
+ } catch (error) {
245
258
  utility.error(error);
246
- });
259
+ }
247
260
  }
248
261
 
249
262
  /**
@@ -252,50 +265,44 @@ function createTask(taskData, columnName) {
252
265
  * @param {string} columnName
253
266
  */
254
267
  async function addUntrackedTasks(untrackedTasks, columnName) {
255
- for (let untrackedTask of untrackedTasks) {
256
- try {
257
- await kanbn.addUntrackedTaskToIndex(untrackedTask, columnName);
258
- } catch (error) {
259
- utility.error(error);
260
- return;
268
+ for (const target of boardTargets) {
269
+
270
+ // The column chosen interactively or on the command line applies to the first board; the others
271
+ // use whatever resolveBoardTargets() paired them with
272
+ const targetColumn = target === boardTargets[0] ? columnName : target.columnName;
273
+ for (let untrackedTask of untrackedTasks) {
274
+ try {
275
+ await target.kanbn.addUntrackedTaskToIndex(untrackedTask, targetColumn);
276
+ } catch (error) {
277
+ utility.error(error);
278
+ return;
279
+ }
261
280
  }
281
+ console.log(
282
+ `Added ${untrackedTasks.length} task${untrackedTasks.length !== 1 ? 's' : ''} ` +
283
+ `to column "${targetColumn}"` + (boardTargets.length > 1 || !target.main ? ` on board "${target.slug}"` : '')
284
+ );
262
285
  }
263
- console.log(
264
- `Added ${untrackedTasks.length} task${untrackedTasks.length !== 1 ? 's' : ''} to column "${columnName}"`
265
- );
266
286
  }
267
287
 
268
288
  module.exports = async args => {
269
289
 
270
- // Make sure kanbn has been initialised
271
- if (!await kanbn.initialised()) {
272
- utility.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
290
+ // Work out which boards this command targets, and which column to use on each. -b is repeatable,
291
+ // so one task can be created and placed on several boards at once
292
+ const targets = await utility.resolveBoardTargets(workspace, args);
293
+ if (targets === null) {
273
294
  return;
274
295
  }
275
-
276
- // Get the index and make sure it has some columns
277
- let index;
278
- try {
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;
296
+ boardTargets = targets;
297
+ kanbn = targets[0].kanbn;
298
+ for (const target of boardTargets) {
299
+ target.main = await target.kanbn.isMainBoard();
288
300
  }
301
+ utility.showColumnFallbackNotices(boardTargets, kanbn.isVerbose(targets[0].index));
289
302
 
290
- // Get column name if specified, otherwise default to the first available column
291
- let columnName = columnNames[0];
292
- if (args.column) {
293
- columnName = utility.strArg(args.column);
294
- if (columnNames.indexOf(columnName) === -1) {
295
- utility.error(`Column "${columnName}" doesn't exist`);
296
- return;
297
- }
298
- }
303
+ const index = targets[0].index;
304
+ const columnNames = Object.keys(index.columns);
305
+ const columnName = targets[0].columnName;
299
306
 
300
307
  // Add untracked file(s)
301
308
  if (args.untracked) {
@@ -534,8 +541,7 @@ module.exports = async args => {
534
541
  type: relation.type
535
542
  }));
536
543
  }
537
- columnName = answers.column;
538
- createTask(taskData, columnName);
544
+ createTask(taskData, answers.column);
539
545
  })
540
546
  .catch(error => {
541
547
  utility.error(error);
@@ -1,6 +1,9 @@
1
- const kanbn = require('../main');
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
@@ -32,11 +35,12 @@ function listArchivedTasks() {
32
35
 
33
36
  module.exports = async args => {
34
37
 
35
- // Make sure kanbn has been initialised
36
- if (!await kanbn.initialised()) {
37
- utility.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
38
+ // Work out which board this command targets
39
+ const scoped = await utility.resolveBoard(workspace, args);
40
+ if (scoped === null) {
38
41
  return;
39
42
  }
43
+ kanbn = scoped;
40
44
 
41
45
  // Check if we're listing archived tasks
42
46
  if (args.list) {
@@ -1,12 +1,12 @@
1
- const kanbn = require('../main');
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
- // Make sure kanbn has been initialised
8
- if (!await kanbn.initialised()) {
9
- utility.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
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
 
@@ -32,6 +32,11 @@ module.exports = async args => {
32
32
  // Show the board
33
33
  board
34
34
  .show(index, tasks, args.view, args.json)
35
+ .then(async () => {
36
+ const verbose = !args.json && kanbn.isVerbose(index);
37
+ utility.showDateDriftHint(kanbn.getDateDrift(index, tasks), verbose);
38
+ utility.showBoardCountHint(await workspace.listBoards(), verbose);
39
+ })
35
40
  .catch(error => {
36
41
  utility.error(error);
37
42
  });
@@ -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 kanbn = require('../main');
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
- // Make sure kanbn has been initialised
66
- if (!await kanbn.initialised()) {
67
- utility.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
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
@@ -127,7 +131,7 @@ module.exports = async args => {
127
131
 
128
132
  // Render chart
129
133
  const PADDING = ' ';
130
- const width = Math.max(1, term.width - (PADDING.length + 1));
134
+ const width = Math.max(1, utility.terminalWidth(term.width) - (PADDING.length + 1));
131
135
 
132
136
  const plots = [];
133
137
  for (const s of data.series) {
@@ -1,7 +1,10 @@
1
- const kanbn = require('../main');
1
+ const workspace = require('../main');
2
2
  const utility = require('../utility');
3
3
  const inquirer = require('inquirer');
4
- const getGitUsername = require('git-user-name');
4
+ const getGitUsername = require('../git-user-name');
5
+
6
+ // The board this command targets - set from --board before anything else runs
7
+ let kanbn = workspace;
5
8
 
6
9
  /**
7
10
  * Add a comment interactively
@@ -51,11 +54,12 @@ function addComment(taskId, text, author) {
51
54
 
52
55
  module.exports = async args => {
53
56
 
54
- // Make sure kanbn has been initialised
55
- if (!await kanbn.initialised()) {
56
- utility.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
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];
@@ -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 fuzzy = require('fuzzy');
5
5
  const chrono = require('chrono-node');
6
- const getGitUsername = require('git-user-name');
6
+ const getGitUsername = require('../git-user-name');
7
+
8
+ // The board this command targets - set from --board before anything else runs
9
+ let kanbn = workspace;
7
10
 
8
11
  inquirer.registerPrompt('datepicker', require('inquirer-datepicker'));
9
12
  inquirer.registerPrompt('recursive', require('inquirer-recursive'));
@@ -412,7 +415,7 @@ async function interactive(taskData, taskIds, columnName, columnNames) {
412
415
  type: 'input',
413
416
  name: 'type',
414
417
  message: 'Relation type:',
415
- default: answers => taskData.relations.find(relation => relation.task === answers.selectRelation).task,
418
+ default: answers => taskData.relations.find(relation => relation.task === answers.selectRelation).type,
416
419
  when: answers => answers.editRelation === 'edit'
417
420
  }
418
421
  ]
@@ -420,15 +423,31 @@ async function interactive(taskData, taskIds, columnName, columnNames) {
420
423
  ]);
421
424
  }
422
425
 
426
+ /**
427
+ * Metadata fields that can be cleared with --unset, in addition to any custom fields defined in
428
+ * the index options
429
+ */
430
+ const UNSETTABLE_FIELDS = [
431
+ 'started',
432
+ 'completed',
433
+ 'due',
434
+ 'plannedStart',
435
+ 'plannedFinish',
436
+ 'assigned',
437
+ 'progress',
438
+ 'tags'
439
+ ];
440
+
423
441
  /**
424
442
  * Update a task
425
443
  * @param {string} taskId
426
444
  * @param {object} taskData
427
445
  * @param {?string} columnName
446
+ * @param {string[]} [unsetFields=[]]
428
447
  */
429
- function updateTask(taskId, taskData, columnName) {
448
+ function updateTask(taskId, taskData, columnName, unsetFields = []) {
430
449
  kanbn
431
- .updateTask(taskId, taskData, columnName)
450
+ .updateTask(taskId, taskData, columnName, unsetFields)
432
451
  .then(taskId => {
433
452
  console.log(`Updated task "${taskId}"`);
434
453
  })
@@ -439,11 +458,12 @@ function updateTask(taskId, taskData, columnName) {
439
458
 
440
459
  module.exports = async args => {
441
460
 
442
- // Make sure kanbn has been initialised
443
- if (!await kanbn.initialised()) {
444
- utility.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
461
+ // Work out which board this command targets
462
+ const scoped = await utility.resolveBoard(workspace, args);
463
+ if (scoped === null) {
445
464
  return;
446
465
  }
466
+ kanbn = scoped;
447
467
 
448
468
  // Get the task that we're editing
449
469
  const taskId = args._[1];
@@ -578,8 +598,8 @@ module.exports = async args => {
578
598
 
579
599
  // Check that the sub-tasks being removed currently exist
580
600
  for (let removedSubTask of removedSubTasks) {
581
- if (taskData.subTasks.find(subTask => subTask.text === removedSubTask.text) === undefined) {
582
- utility.error(`Sub-task "${removedSubTask.text}" doesn't exist`);
601
+ if (taskData.subTasks.find(subTask => subTask.text === removedSubTask) === undefined) {
602
+ utility.error(`Sub-task "${removedSubTask}" doesn't exist`);
583
603
  return;
584
604
  }
585
605
  }
@@ -633,7 +653,7 @@ module.exports = async args => {
633
653
  // Check that the tags being removed currently exist
634
654
  for (let removedTag of removedTags) {
635
655
  if (taskData.metadata.tags.indexOf(removedTag) === -1) {
636
- utility.error(`Tag "${removedSubTask.text}" doesn't exist`);
656
+ utility.error(`Tag "${removedTag}" doesn't exist`);
637
657
  return;
638
658
  }
639
659
  }
@@ -652,8 +672,8 @@ module.exports = async args => {
652
672
 
653
673
  // Check that the relations being removed currently exist
654
674
  for (let removedRelation of removedRelations) {
655
- if (taskData.relations.find(relation => relation.task === removedRelation.task) === undefined) {
656
- utility.error(`Relation "${removedRelation.task}" doesn't exist`);
675
+ if (taskData.relations.find(relation => relation.task === removedRelation) === undefined) {
676
+ utility.error(`Relation "${removedRelation}" doesn't exist`);
657
677
  return;
658
678
  }
659
679
  }
@@ -754,6 +774,43 @@ module.exports = async args => {
754
774
  }
755
775
  }
756
776
 
777
+ // Unset metadata fields
778
+ const unsetFields = [];
779
+ if (args.unset) {
780
+ const customFieldNames = 'customFields' in index.options
781
+ ? index.options.customFields.map(customField => customField.name)
782
+ : [];
783
+ const unsettableFields = [...UNSETTABLE_FIELDS, ...customFieldNames];
784
+
785
+ // Arguments that set the value of a metadata field, so that setting and unsetting the same
786
+ // field in one command can be rejected instead of silently resolving one way or the other
787
+ const settingArgs = {
788
+ due: args.due,
789
+ plannedStart: args.plannedStart,
790
+ plannedFinish: args.plannedFinish,
791
+ progress: args.progress,
792
+ assigned: args.assigned,
793
+ tags: args.tag,
794
+ ...Object.fromEntries(customFieldNames.map(name => [name, args[name]]))
795
+ };
796
+
797
+ for (let field of utility.arrayArg(args.unset)) {
798
+ if (unsettableFields.indexOf(field) === -1) {
799
+ utility.error(
800
+ `Cannot unset "${field}"\nAvailable fields: ${unsettableFields.join(', ')}`
801
+ );
802
+ return;
803
+ }
804
+ if (settingArgs[field] !== undefined) {
805
+ utility.error(`Cannot set and unset "${field}" in the same command`);
806
+ return;
807
+ }
808
+ if (unsetFields.indexOf(field) === -1) {
809
+ unsetFields.push(field);
810
+ }
811
+ }
812
+ }
813
+
757
814
  // Update task interactively
758
815
  if (args.interactive) {
759
816
  interactive(taskData, taskIds, columnName, columnNames)
@@ -810,7 +867,7 @@ module.exports = async args => {
810
867
  // Edit or remove sub-tasks
811
868
  if ('editSubTasks' in answers) {
812
869
  for (editSubTask of answers.editSubTasks) {
813
- const i = taskData.subTasks.findIndex(subTask => subTask.task === editSubTask.selectSubTask);
870
+ const i = taskData.subTasks.findIndex(subTask => subTask.text === editSubTask.selectSubTask);
814
871
  if (i !== -1) {
815
872
  switch (editSubTask.editSubTask) {
816
873
  case 'remove':
@@ -837,7 +894,7 @@ module.exports = async args => {
837
894
  // Remove tags
838
895
  if ('removeTags' in answers && 'metadata' in taskData && 'tags' in taskData.metadata) {
839
896
  for (removeTag of answers.removeTags) {
840
- const i = taskData.metadata.tags.indexOf(removeTag.name);
897
+ const i = taskData.metadata.tags.indexOf(removeTag.selectTag);
841
898
  if (i !== -1) {
842
899
  taskData.metadata.tags.splice(i, 1);
843
900
  }
@@ -878,7 +935,7 @@ module.exports = async args => {
878
935
 
879
936
  // Update task
880
937
  columnName = answers.column !== currentColumnName ? answers.column : null;
881
- updateTask(taskId, taskData, columnName);
938
+ updateTask(taskId, taskData, columnName, unsetFields);
882
939
  })
883
940
  .catch(error => {
884
941
  utility.error(error);
@@ -887,6 +944,6 @@ module.exports = async args => {
887
944
  // Otherwise edit task non-interactively
888
945
  } else {
889
946
  columnName = columnName !== currentColumnName ? columnName : null;
890
- updateTask(taskId, taskData, columnName);
947
+ updateTask(taskId, taskData, columnName, unsetFields);
891
948
  }
892
949
  };
@@ -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 = [
@@ -213,12 +216,11 @@ async function interactive() {
213
216
  * @param {boolean} quiet
214
217
  * @param {boolean} json
215
218
  */
216
- function findTasks(filters, quiet, json) {
219
+ function findTasks(filters, quiet, json, allBoards = false) {
217
220
  const removeEmptyProperties = o => Object.fromEntries(Object.entries(o).filter(
218
221
  ([k, v]) => !(Array.isArray(v) && v.length == 0) && !!v
219
222
  ));
220
- kanbn
221
- .search(filters, quiet)
223
+ (allBoards ? kanbn.searchAllBoards(filters, quiet) : kanbn.search(filters, quiet))
222
224
  .then(results => {
223
225
  if (quiet) {
224
226
  console.log(json ? JSON.stringify(results, null, 2) : results.join('\n'));
@@ -309,11 +311,12 @@ function convertDateFilters(filters, filterName) {
309
311
 
310
312
  module.exports = async args => {
311
313
 
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}');
314
+ // Work out which board this command targets
315
+ const scoped = await utility.resolveBoard(workspace, args);
316
+ if (scoped === null) {
315
317
  return;
316
318
  }
319
+ kanbn = scoped;
317
320
 
318
321
  // Get the index and make sure it has some columns
319
322
  let index;
@@ -487,7 +490,7 @@ module.exports = async args => {
487
490
  filter.value
488
491
  );
489
492
  }
490
- findTasks(filters, !otherAnswers.nonquiet, otherAnswers.json);
493
+ findTasks(filters, !otherAnswers.nonquiet, otherAnswers.json, !!args['all-boards']);
491
494
  })
492
495
  .catch(error => {
493
496
  utility.error(error);
@@ -499,6 +502,6 @@ module.exports = async args => {
499
502
 
500
503
  // Otherwise create task non-interactively
501
504
  } else {
502
- findTasks(filters, args.quiet, args.json);
505
+ findTasks(filters, args.quiet, args.json, !!args['all-boards']);
503
506
  }
504
507
  };
@@ -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,11 +316,12 @@ 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
327
  // Get assigned