@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
@@ -24,6 +24,23 @@ function validateOptions(options) {
24
24
  type: 'array',
25
25
  items: { type: 'string' }
26
26
  },
27
+ 'startedField': { type: 'string' },
28
+ 'completedField': { type: 'string' },
29
+ 'verbose': { type: 'boolean' },
30
+ 'defaultBoard': { type: 'string' },
31
+ 'boards': {
32
+ type: 'object',
33
+ properties: {
34
+ 'exclude': {
35
+ type: 'array',
36
+ items: { type: 'string' }
37
+ },
38
+ 'order': {
39
+ type: 'array',
40
+ items: { type: 'string' }
41
+ }
42
+ }
43
+ },
27
44
  'sprints': {
28
45
  type: 'array',
29
46
  items: {
@@ -26,7 +26,9 @@ module.exports = function parseMarkdown(markdown) {
26
26
  parsed[headings[i].title] = {
27
27
  heading: headings[i].heading,
28
28
  content: markdown.slice(
29
- headings[i].index + headings[i].heading.length + 1,
29
+ // The + 1 skips the newline that ends the heading line; the synthetic 'raw' entry for
30
+ // content before the first heading has no heading line, so it starts at index 0
31
+ headings[i].index + headings[i].heading.length + (headings[i].heading ? 1 : 0),
30
32
  i < headings.length - 1
31
33
  ? headings[i + 1].index
32
34
  : undefined
package/src/parse-task.js CHANGED
@@ -200,6 +200,7 @@ function validateHistoryFromMarkdown(history) {
200
200
  'column': { type: 'string' },
201
201
  'fromColumn': { type: 'string' },
202
202
  'toColumn': { type: 'string' },
203
+ 'board': { type: 'string' },
203
204
  'author': { type: 'string' },
204
205
  'fromProgress': { oneOf: [{ type: 'number' }, { type: 'string' }] },
205
206
  'toProgress': { oneOf: [{ type: 'number' }, { type: 'string' }] }
@@ -226,6 +227,7 @@ function validateHistoryFromJSON(history) {
226
227
  'column': { type: 'string' },
227
228
  'fromColumn': { type: 'string' },
228
229
  'toColumn': { type: 'string' },
230
+ 'board': { type: 'string' },
229
231
  'author': { type: 'string' },
230
232
  'fromProgress': { type: 'number' },
231
233
  'toProgress': { type: 'number' }
@@ -239,6 +241,11 @@ function validateHistoryFromJSON(history) {
239
241
 
240
242
  /**
241
243
  * Validate required fields in a history event
244
+ *
245
+ * An event with an unrecognised type is left alone rather than rejected: a task file written by a
246
+ * newer version of kanbn may contain event types this version doesn't know about, and refusing to
247
+ * parse the whole task would make the file unreadable. Unknown events are preserved on write and
248
+ * ignored when history is replayed.
242
249
  * @param {object} historyEvent
243
250
  */
244
251
  function validateHistoryEvent(historyEvent) {
@@ -254,6 +261,13 @@ function validateHistoryEvent(historyEvent) {
254
261
  throw new Error('created history event is missing column');
255
262
  }
256
263
  break;
264
+ case 'added':
265
+ if (!historyEvent.column) {
266
+ throw new Error('added history event is missing column');
267
+ }
268
+ break;
269
+ case 'removed':
270
+ break;
257
271
  case 'moved':
258
272
  if (!historyEvent.fromColumn || !historyEvent.toColumn) {
259
273
  throw new Error('moved history event is missing fromColumn or toColumn');
@@ -275,10 +289,25 @@ function validateHistoryEvent(historyEvent) {
275
289
  }
276
290
  break;
277
291
  default:
278
- throw new Error(`unsupported history event type "${historyEvent.type}"`);
292
+ break;
279
293
  }
280
294
  }
281
295
 
296
+ /**
297
+ * History event keys that this version understands, in the order they should be written
298
+ */
299
+ const KNOWN_HISTORY_EVENT_KEYS = [
300
+ 'type',
301
+ 'date',
302
+ 'column',
303
+ 'fromColumn',
304
+ 'toColumn',
305
+ 'board',
306
+ 'fromProgress',
307
+ 'toProgress',
308
+ 'author'
309
+ ];
310
+
282
311
  module.exports = {
283
312
 
284
313
  /**
@@ -651,6 +680,12 @@ module.exports = {
651
680
  if ('toColumn' in historyEvent) {
652
681
  historyEventOutput.push(`toColumn: ${historyEvent.toColumn}`);
653
682
  }
683
+
684
+ // Events on the main board carry no board key, so existing task files - and files
685
+ // written by single-board workspaces - are byte-identical to what they always were
686
+ if ('board' in historyEvent && historyEvent.board) {
687
+ historyEventOutput.push(`board: ${historyEvent.board}`);
688
+ }
654
689
  if ('fromProgress' in historyEvent) {
655
690
  historyEventOutput.push(`fromProgress: ${historyEvent.fromProgress}`);
656
691
  }
@@ -660,6 +695,14 @@ module.exports = {
660
695
  if ('author' in historyEvent && historyEvent.author) {
661
696
  historyEventOutput.push(`author: ${historyEvent.author}`);
662
697
  }
698
+
699
+ // Write back any keys this version doesn't recognise, so that a file written by a
700
+ // newer version of kanbn survives a read/write cycle here without losing data
701
+ for (const key of Object.keys(historyEvent)) {
702
+ if (KNOWN_HISTORY_EVENT_KEYS.indexOf(key) === -1 && historyEvent[key] !== undefined) {
703
+ historyEventOutput.push(`${key}: ${historyEvent[key]}`);
704
+ }
705
+ }
663
706
  return `- ${historyEventOutput.map((v, i) => i > 0 ? ` ${v}` : v).join('\n')}`;
664
707
  }).join('\n')
665
708
  );
package/src/utility.js CHANGED
@@ -19,6 +19,155 @@ module.exports = (() => {
19
19
  !dontExit && process.env.KANBN_ENV !== 'test' && process.exit(1);
20
20
  },
21
21
 
22
+ /**
23
+ * Resolve the board a command targets and return a Kanbn instance scoped to it, printing an
24
+ * error and returning null if the workspace or the board doesn't exist
25
+ * @param {object} workspace The workspace-scoped Kanbn instance
26
+ * @param {object} args The parsed command arguments
27
+ * @return {Promise<object|null>} A board-scoped Kanbn instance, or null if it can't be resolved
28
+ */
29
+ async resolveBoard(workspace, args) {
30
+
31
+ // Sprint numbers and names are relative to one board's list, so an aggregate across boards
32
+ // can't resolve them. Refuse rather than guess
33
+ if (args['all-boards'] && args.sprint) {
34
+ this.error(
35
+ '{b}--sprint{b} and {b}--all-boards{b} cannot be used together, because sprint numbers ' +
36
+ 'and names are relative to a single board'
37
+ );
38
+ return null;
39
+ }
40
+ const kanbn = await workspace.boardFromArgs(args);
41
+ if (!await kanbn.workspaceInitialised()) {
42
+ this.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
43
+ return null;
44
+ }
45
+ if (!await kanbn.initialised()) {
46
+ this.error(
47
+ `Board "${await kanbn.resolveBoardSlug()}" doesn't exist\n` +
48
+ 'Try running: {b}kanbn boards{b}'
49
+ );
50
+ return null;
51
+ }
52
+ return kanbn;
53
+ },
54
+
55
+ /**
56
+ * Resolve the boards a command targets, along with the column to use on each. `-b` is repeatable
57
+ * and pairs positionally with `-c`: board i uses the i-th `-c` if there is one, otherwise the
58
+ * first `-c`, otherwise that board's first column (Q5). Prints an error and returns null if any
59
+ * board can't be resolved
60
+ * @param {object} workspace The workspace-scoped Kanbn instance
61
+ * @param {object} args The parsed command arguments
62
+ * @return {Promise<object[]|null>} One entry per target board, or null on error
63
+ */
64
+ async resolveBoardTargets(workspace, args) {
65
+ const boardArgs = args.board === undefined
66
+ ? []
67
+ : this.arrayArg(args.board).filter(slug => slug !== undefined && slug !== '');
68
+ const columnArgs = args.column === undefined ? [] : this.arrayArg(args.column);
69
+ const slugs = boardArgs.length ? boardArgs : [await workspace.resolveTargetBoard(null)];
70
+
71
+ // With a single target board an unknown column is an error, exactly as it has always been.
72
+ // Falling back only makes sense when several boards are targeted at once, where a column can
73
+ // legitimately exist on one and not another
74
+ const allowFallback = slugs.length > 1;
75
+
76
+ const targets = [];
77
+ for (let i = 0; i < slugs.length; i++) {
78
+ const kanbn = await this.resolveBoard(workspace, { board: slugs[i] });
79
+ if (kanbn === null) {
80
+ return null;
81
+ }
82
+ let index;
83
+ try {
84
+ index = await kanbn.getIndex();
85
+ } catch (error) {
86
+ this.error(error);
87
+ return null;
88
+ }
89
+ const columnNames = Object.keys(index.columns);
90
+ if (!columnNames.length) {
91
+ this.error('No columns defined in the index\nTry running {b}kanbn init -c "column name"{b}');
92
+ return null;
93
+ }
94
+
95
+ const requestedColumn = columnArgs.length
96
+ ? (columnArgs[i] !== undefined ? columnArgs[i] : columnArgs[0])
97
+ : null;
98
+ let columnName = requestedColumn === null ? columnNames[0] : requestedColumn;
99
+ let fallback = false;
100
+ if (!(columnName in index.columns)) {
101
+ if (!allowFallback) {
102
+ this.error(`Column "${columnName}" doesn't exist`);
103
+ return null;
104
+ }
105
+ columnName = columnNames[0];
106
+ fallback = true;
107
+ }
108
+ targets.push({
109
+ kanbn,
110
+ index,
111
+ slug: await kanbn.resolveBoardSlug(),
112
+ columnName,
113
+ requestedColumn,
114
+ fallback
115
+ });
116
+ }
117
+ return targets;
118
+ },
119
+
120
+ /**
121
+ * Tell the user when a target board didn't have the column that was asked for, so that a task
122
+ * quietly landing somewhere else is never a surprise
123
+ * @param {object[]} targets Targets from resolveBoardTargets()
124
+ * @param {boolean} show False to suppress the notice
125
+ */
126
+ showColumnFallbackNotices(targets, show = true) {
127
+ if (!show) {
128
+ return;
129
+ }
130
+ for (const target of targets.filter(t => t.fallback)) {
131
+ console.log(this.replaceTags(
132
+ `{d}Board "${target.slug}" has no column "${target.requestedColumn}" - using "${target.columnName}"{d}`
133
+ ));
134
+ }
135
+ },
136
+
137
+ /**
138
+ * Show a hint about how many boards this workspace has, so that a command showing one board
139
+ * doesn't look like it's showing everything. Suppressed for machine-readable output and when the
140
+ * board sets verbose to false
141
+ * @param {object[]} boards The list of boards
142
+ * @param {boolean} show False to suppress the hint
143
+ */
144
+ showBoardCountHint(boards, show = true) {
145
+ if (!show || boards.length < 2) {
146
+ return;
147
+ }
148
+ console.log(this.replaceTags(`\n{d}${boards.length} boards available -{d} {b}kanbn boards{b}`));
149
+ },
150
+
151
+ /**
152
+ * Show a hint about tasks whose dates don't match the column they're in. Hints are suppressed
153
+ * for machine-readable output and when the board sets verbose to false
154
+ * @param {object[]} drift Drift entries from kanbn.getDateDrift()
155
+ * @param {boolean} show False to suppress the hint
156
+ */
157
+ showDateDriftHint(drift, show = true) {
158
+ if (!show || !drift.length) {
159
+ return;
160
+ }
161
+ const fixable = drift.filter(d => d.fixable).length;
162
+ const summary =
163
+ `${drift.length} ${drift.length === 1 ? 'task has' : 'tasks have'} dates that don't match ` +
164
+ `${drift.length === 1 ? 'its' : 'their'} column`;
165
+ console.log(this.replaceTags(
166
+ `\n{d}${summary}${fixable ? ' - run{d} {b}kanbn validate --fix{b}' : '{d}'}` +
167
+ (fixable ? ` {d}to fill in the missing ${fixable === 1 ? 'date' : 'dates'}{d}` : '')
168
+ ));
169
+ },
170
+
22
171
  /**
23
172
  * Convert a string to simplified paramcase, e.g:
24
173
  * PascalCase -> pascalcase
@@ -34,7 +183,12 @@ module.exports = (() => {
34
183
  )
35
184
  .split(/[\s!?.,@:;|\\/"'`£$%\^&*{}[\]()<>~#+\-=_¬]+/g)
36
185
  .join('-')
37
- .replace(/(^-|-$)/g, '');
186
+ .replace(/(^-|-$)/g, '')
187
+ // The word-boundary replacement above only matches an uppercase run that is followed by
188
+ // another character, so a trailing capital ("Task A" -> "task-A") and any non-ASCII capital
189
+ // ("Ünïcodé" -> "Ünïcodé") would otherwise survive. Board slugs use this function, so ids
190
+ // must be lowercase throughout.
191
+ .toLowerCase();
38
192
  },
39
193
 
40
194
  /**
@@ -141,6 +295,17 @@ module.exports = (() => {
141
295
  return s;
142
296
  },
143
297
 
298
+ /**
299
+ * Coerce a terminal width into a usable number. The terminal width is unavailable when stdout
300
+ * isn't a TTY (i.e. when the output is piped or redirected), so fall back to a default
301
+ * @param {any} width The reported terminal width
302
+ * @param {number} [fallback=80] The width to use when the reported width is unusable
303
+ * @return {number} A usable terminal width
304
+ */
305
+ terminalWidth(width, fallback = 80) {
306
+ return Number.isInteger(width) && width > 0 ? width : fallback;
307
+ },
308
+
144
309
  /**
145
310
  * Zip 2 arrays together, i.e. ([1, 2, 3], [a, b, c]) => [[1, a], [2, b], [3, c]]
146
311
  * @param {any[]} a
package/PLANS.md DELETED
@@ -1,9 +0,0 @@
1
- # Future plans
2
-
3
- * multiple boards
4
- * epics / stories
5
- * swimlanes
6
- * list of contributors in index config
7
- * dropdown in vsce taskeditor
8
- * optional autopopulate from git user
9
- * web UI
File without changes