@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
package/src/main.d.ts CHANGED
@@ -4,14 +4,66 @@ declare type config = {
4
4
  mainFolder?: string,
5
5
  indexFile?: string,
6
6
  taskFolder?: string,
7
- archiveFolder?: string
7
+ archiveFolder?: string,
8
+ defaultBoard?: string,
9
+ boards?: {
10
+ exclude?: string[],
11
+ order?: string[],
12
+ [slug: string]: any
13
+ }
14
+ };
15
+
16
+ declare type board = {
17
+ slug: string,
18
+ path: string,
19
+ name: string,
20
+ description: string,
21
+ main: boolean
22
+ };
23
+
24
+ declare type boardSummary = board & {
25
+ columns: number,
26
+ tasks: number,
27
+ completed: number,
28
+ completedPercentage: number,
29
+ modified: Date | null
30
+ };
31
+
32
+ declare type columnContentEntry = {
33
+ position: number,
34
+ text: string,
35
+ raw: string,
36
+ block?: boolean
8
37
  };
9
38
 
10
39
  declare type index = {
11
40
  name: string,
12
41
  description: string,
13
42
  options: Record<string, any>,
14
- columns: Record<string, string[]>
43
+ columns: Record<string, string[]>,
44
+
45
+ // Lines in a column that aren't task links. Preserved verbatim and ignored by everything else
46
+ columnContent?: Record<string, columnContentEntry[]>
47
+ };
48
+
49
+ declare type missingTaskFile = {
50
+ task: string,
51
+ column: string
52
+ };
53
+
54
+ declare type simpleTask = {
55
+ column: string,
56
+ position: number,
57
+ text: string,
58
+ raw: string
59
+ };
60
+
61
+ declare type columnContentWarning = {
62
+ board: string,
63
+ column: string,
64
+ type: 'non-task-line' | 'untracked-task-line' | 'malformed-task-link',
65
+ text: string,
66
+ message: string
15
67
  };
16
68
 
17
69
  declare type subTask = {
@@ -58,12 +110,99 @@ declare type sprint = {
58
110
  description?: string
59
111
  };
60
112
 
113
+ declare type contributor = {
114
+ name: string,
115
+ displayName: string,
116
+ aliases: string[],
117
+ email?: string,
118
+ colour?: string
119
+ };
120
+
121
+ declare type contributorSpelling = {
122
+ value: string,
123
+ assigned: number,
124
+ comments: number,
125
+ tasks: string[]
126
+ };
127
+
128
+ declare type contributorUsage = {
129
+ contributors: (contributor & {
130
+ assigned: number,
131
+ comments: number,
132
+ tasks: number,
133
+ spellings: contributorSpelling[]
134
+ })[],
135
+ unknown: contributorSpelling[]
136
+ };
137
+
138
+ declare type contributorWarning = {
139
+ task: string,
140
+ type: 'unknown-contributor',
141
+ value: string,
142
+ message: string
143
+ };
144
+
145
+ declare type actionRule = {
146
+ name?: string,
147
+ on: string,
148
+ when?: Record<string, any>,
149
+ for?: {
150
+ related?: string,
151
+ direction?: 'incoming' | 'outgoing',
152
+ where?: Record<string, any>
153
+ },
154
+ anyBoard?: boolean,
155
+ then: Record<string, any>[]
156
+ };
157
+
158
+ declare type actionWarning = {
159
+ type: 'conflicting-actions' | 'unresolvable-user',
160
+ message: string
161
+ };
162
+
61
163
  export class Kanbn {
62
- constructor(root?: any);
164
+ constructor(root?: any, options?: { board?: string, caches?: any, actions?: boolean });
63
165
  ROOT: string;
64
166
  CONFIG_YAML: string;
65
167
  CONFIG_JSON: string;
66
168
  configMemo: any;
169
+ /**
170
+ * The board this instance is scoped to, or null for the main board
171
+ */
172
+ boardSlug: string | null;
173
+ /**
174
+ * Rules that were skipped during the last operation, for the caller to report
175
+ */
176
+ lastActionWarnings: string[];
177
+ /**
178
+ * Whether scripted actions run for operations on this instance
179
+ */
180
+ actionsEnabled: boolean;
181
+ /**
182
+ * Get an instance that runs no actions
183
+ */
184
+ withoutActions(): Kanbn;
185
+ /**
186
+ * Get the action rules that apply to this board
187
+ */
188
+ getActionRules(index?: index | null): Promise<actionRule[]>;
189
+ /**
190
+ * Check whether actions should run at all
191
+ */
192
+ actionsAllowed(): boolean;
193
+ /**
194
+ * Find things about this board's action rules that are legal but probably not what the author meant
195
+ */
196
+ findActionWarnings(): Promise<actionWarning[]>;
197
+ /**
198
+ * Get a copy of this instance scoped to another board, sharing this one's cached config
199
+ * @param {?string} [slug=null] The board slug, or null/"main"/"default" for the main board
200
+ */
201
+ board(slug?: string | null): Kanbn;
202
+ /**
203
+ * Alias for board()
204
+ */
205
+ withBoard(slug?: string | null): Kanbn;
67
206
  /**
68
207
  * Check if a separate config file exists
69
208
  * @returns {Promise<boolean>} True if a config file exists
@@ -108,7 +247,25 @@ export class Kanbn {
108
247
  */
109
248
  getMainFolder(): Promise<string>;
110
249
  /**
111
- * Get the index path
250
+ * Get the main board's slug, i.e. the index file name without its extension
251
+ * @return {Promise<string>} The main board slug
252
+ */
253
+ getMainBoardSlug(): Promise<string>;
254
+ /**
255
+ * Resolve a board slug, mapping the reserved aliases and an absent slug onto the main board
256
+ * @param {?string} [slug] The board slug, defaulting to this instance's board
257
+ */
258
+ resolveBoardSlug(slug?: string | null): Promise<string>;
259
+ /**
260
+ * Check if a slug refers to the main board
261
+ */
262
+ isMainBoard(slug?: string | null): Promise<boolean>;
263
+ /**
264
+ * Get the file path for a board
265
+ */
266
+ getBoardPath(slug?: string | null): Promise<string>;
267
+ /**
268
+ * Get the index path, i.e. the path of the board this instance is scoped to
112
269
  * @return {Promise<string>} The kanbn index path
113
270
  */
114
271
  getIndexPath(): Promise<string>;
@@ -154,6 +311,155 @@ export class Kanbn {
154
311
  * @param {object} indexData Index data to save
155
312
  */
156
313
  saveIndex(indexData: object): Promise<void>;
314
+ /**
315
+ * Get the workspace-scoped options, from the config file or the main board's front matter
316
+ */
317
+ getWorkspaceOptions(): Promise<Record<string, any>>;
318
+ /**
319
+ * Load the workspace options along with where they came from
320
+ */
321
+ loadWorkspaceOptions(): Promise<{ options: Record<string, any>, fromConfig: boolean }>;
322
+ /**
323
+ * Normalise a contributors option into a consistent object form
324
+ */
325
+ normaliseContributors(contributors: any): contributor[];
326
+ /**
327
+ * Get the workspace's contributors, normalised to the object form
328
+ */
329
+ getContributors(): Promise<contributor[]>;
330
+ /**
331
+ * Find the contributor a value refers to, matching name, display name and aliases
332
+ */
333
+ findContributor(value: string | null): Promise<contributor | null>;
334
+ /**
335
+ * Work out who the current user is: KANBN_USER, then a contributor matched by git email or git
336
+ * name, then the git username, then null
337
+ */
338
+ currentUser(): Promise<string | null>;
339
+ /**
340
+ * Collect every distinct `assigned` and comment `author` value used across the workspace's tasks
341
+ */
342
+ collectContributorValues(): Promise<Map<string, { value: string, assigned: number, comments: number, tasks: Set<string> }>>;
343
+ /**
344
+ * Report how contributors are used, and which names in use aren't known contributors
345
+ */
346
+ getContributorUsage(): Promise<contributorUsage>;
347
+ /**
348
+ * Find tasks whose assigned user or comment author isn't a known contributor
349
+ */
350
+ findContributorWarnings(): Promise<contributorWarning[]>;
351
+ /**
352
+ * Get the options a secondary board inherits from the workspace
353
+ */
354
+ getInheritedBoardOptions(): Promise<Record<string, any>>;
355
+ /**
356
+ * Get the per-board options declared for a board under the `boards` key in the config file
357
+ */
358
+ getBoardConfig(slug?: string | null): Promise<Record<string, any>>;
359
+ /**
360
+ * Layer a board's own front matter options over the workspace options
361
+ */
362
+ resolveBoardOptions(resolvedSlug: string, ownOptions: Record<string, any>): Promise<Record<string, any>>;
363
+ /**
364
+ * Work out which options belong in a secondary board's own front matter
365
+ */
366
+ getOwnBoardOptions(resolvedSlug: string, indexData: object): Promise<Record<string, any>>;
367
+ /**
368
+ * Overwrite a board file with the specified data
369
+ */
370
+ saveBoard(slug: string | null, indexData: object): Promise<void>;
371
+ /**
372
+ * Load a board file and parse it to an object
373
+ */
374
+ loadBoard(slug?: string | null): Promise<index>;
375
+ /**
376
+ * Work out which board a command should target: --board, then KANBN_BOARD, then defaultBoard
377
+ */
378
+ resolveTargetBoard(slug?: string | null): Promise<string | null>;
379
+ /**
380
+ * Get a Kanbn instance scoped to the board a command's arguments point at
381
+ */
382
+ boardFromArgs(args?: object): Promise<Kanbn>;
383
+ /**
384
+ * Find all boards in the workspace
385
+ */
386
+ listBoards(): Promise<board[]>;
387
+ /**
388
+ * List boards with column and task counts, completion percentage and last modified date
389
+ */
390
+ getBoardsSummary(): Promise<boardSummary[]>;
391
+ /**
392
+ * Get every task that appears on more than one board, with the column it occupies on each
393
+ */
394
+ getCrossBoardTasks(allTasks?: boolean): Promise<{ id: string, boards: Record<string, string> }[]>;
395
+ /**
396
+ * Check if a board exists
397
+ */
398
+ boardExists(slug: string): Promise<boolean>;
399
+ /**
400
+ * Get the value to record in a history event's `board` key, or null for the main board
401
+ */
402
+ historyBoard(): Promise<string | null>;
403
+ /**
404
+ * Add an existing task to this board
405
+ */
406
+ addTaskToBoard(taskId: string, columnName: string): Promise<string>;
407
+ /**
408
+ * Check if a task file exists, regardless of whether any board references it
409
+ */
410
+ taskFileExists(taskId: string): Promise<boolean>;
411
+ /**
412
+ * Boards named in an archived task's metadata that no longer existed when it was restored
413
+ */
414
+ lastRestoreWarnings: string[];
415
+ /**
416
+ * Create a new board
417
+ */
418
+ createBoard(slug: string, options?: object): Promise<string>;
419
+ /**
420
+ * Create a secondary board, or update an existing one
421
+ */
422
+ initialiseBoard(slug: string, options?: object): Promise<string>;
423
+ /**
424
+ * Delete a board file, returning the ids of tasks that are no longer on any board
425
+ */
426
+ deleteBoard(slug: string): Promise<string[]>;
427
+ /**
428
+ * Find the tasks that would become untracked if a board were deleted
429
+ */
430
+ findOrphanedTasks(slug: string): Promise<string[]>;
431
+ /**
432
+ * Rename a board
433
+ */
434
+ renameBoard(slug: string, newSlug: string, newName?: string | null): Promise<string>;
435
+ /**
436
+ * Find every board that references a task, and the column it occupies on each
437
+ */
438
+ findTaskBoards(taskId: string): Promise<Record<string, string>>;
439
+ /**
440
+ * Alias for findTaskBoards()
441
+ */
442
+ getTaskBoardColumns(taskId: string): Promise<Record<string, string>>;
443
+ /**
444
+ * Get the slugs that can't be used for a board
445
+ */
446
+ getReservedBoardSlugs(): Promise<string[]>;
447
+ /**
448
+ * Check that a slug can be used for a new board, throwing if it can't
449
+ */
450
+ validateBoardSlug(slug: string): Promise<string>;
451
+ /**
452
+ * Get the boards config from the config file, i.e. the exclude list and display order
453
+ */
454
+ getBoardsConfig(): Promise<{ exclude: string[], order: string[] }>;
455
+ /**
456
+ * Find tasks that no board references at all
457
+ */
458
+ findWorkspaceUntrackedTasks(): Promise<Set<string>>;
459
+ /**
460
+ * Find tasks that other boards track but this one doesn't
461
+ */
462
+ findTasksOnOtherBoards(): Promise<Record<string, Record<string, string>>>;
157
463
  /**
158
464
  * Load the index file and parse it to an object
159
465
  * @return {Promise<object>} The index object
@@ -172,12 +478,79 @@ export class Kanbn {
172
478
  */
173
479
  loadTask(taskId: string): Promise<object>;
174
480
  /**
175
- * Load all tracked tasks and return an array of task objects
481
+ * Load all tracked tasks and return an array of task objects. A task the index references but
482
+ * which has no task file is skipped rather than throwing - see findMissingTaskFiles()
176
483
  * @param {object} index The index object
177
484
  * @param {?string} [columnName=null] The optional column name to filter tasks by
178
485
  * @return {Promise<object[]>} All tracked tasks
179
486
  */
180
487
  loadAllTrackedTasks(index: object, columnName?: string | null): Promise<object[]>;
488
+ /**
489
+ * Find tasks that this board references but which have no task file
490
+ * @param {?object} [index=null] The index object, or null to load it
491
+ * @return {Promise<missingTaskFile[]>} A list of the missing tasks and the columns they're in
492
+ */
493
+ findMissingTaskFiles(index?: object | null): Promise<missingTaskFile[]>;
494
+ /**
495
+ * Find lines in this board's columns that aren't task links
496
+ * @param {?object} [index=null] The index object, or null to load it
497
+ * @return {Promise<columnContentWarning[]>} A list of warnings
498
+ */
499
+ findColumnContentWarnings(index?: object | null): Promise<columnContentWarning[]>;
500
+ /**
501
+ * Get this board's simple tasks - lines in a column that aren't task links. Real tasks always win,
502
+ * so callers should resolve a task id first and only fall back to this when nothing matched
503
+ * @param {?string} [input=null] A title to match, or null for every simple task on this board
504
+ * @param {?object} [index=null] The index object, or null to load it
505
+ * @return {Promise<simpleTask[]>} The matching simple tasks
506
+ */
507
+ findSimpleTasks(input?: string | null, index?: object | null): Promise<simpleTask[]>;
508
+ /**
509
+ * Resolve a string to exactly one simple task on this board, or throw
510
+ * @param {string} input The title to match
511
+ * @param {?object} [index=null] The index object, or null to load it
512
+ * @return {Promise<simpleTask>} The matching simple task
513
+ */
514
+ getSimpleTask(input: string, index?: object | null): Promise<simpleTask>;
515
+ /**
516
+ * Move a simple task to another column on this board
517
+ * @param {string} input The title to match
518
+ * @param {string} columnName The column to move it to
519
+ * @param {?number} [position=null] The position in the target column, or the end of it if null
520
+ * @return {Promise<simpleTask & { toColumn: string }>} The simple task that was moved
521
+ */
522
+ moveSimpleTask(
523
+ input: string,
524
+ columnName: string,
525
+ position?: number | null
526
+ ): Promise<simpleTask & { toColumn: string }>;
527
+ /**
528
+ * Move a simple task from this board onto another one
529
+ * @param {string} input The title to match
530
+ * @param {string} targetSlug The board to move it to
531
+ * @param {?string} [columnName=null] The column on the target board, or its first column if null
532
+ * @param {?number} [position=null] The position in the target column, or the end of it if null
533
+ * @return {Promise<simpleTask & { toBoard: string, toColumn: string }>} The simple task that moved
534
+ */
535
+ moveSimpleTaskToBoard(
536
+ input: string,
537
+ targetSlug: string,
538
+ columnName?: string | null,
539
+ position?: number | null
540
+ ): Promise<simpleTask & { toBoard: string, toColumn: string }>;
541
+ /**
542
+ * Remove a simple task from this board
543
+ * @param {string} input The title to match
544
+ * @return {Promise<simpleTask>} The simple task that was removed
545
+ */
546
+ deleteSimpleTask(input: string): Promise<simpleTask>;
547
+ /**
548
+ * Turn a simple task into a real task file
549
+ * @param {string} input The title to match
550
+ * @param {?string} [columnName=null] The column to create the task in, or its own column if null
551
+ * @return {Promise<string>} The id of the task that was created
552
+ */
553
+ promoteSimpleTask(input: string, columnName?: string | null): Promise<string>;
181
554
  /**
182
555
  * Load a task file from the archive and parse it to an object
183
556
  * @param {string} taskId The task id
@@ -201,6 +574,11 @@ export class Kanbn {
201
574
  * @return {Promise<boolean>} True if the current working directory has been initialised, otherwise false
202
575
  */
203
576
  initialised(): Promise<boolean>;
577
+ /**
578
+ * Check if the workspace has been initialised, regardless of which board this instance is scoped to
579
+ * @return {Promise<boolean>} True if the main board exists
580
+ */
581
+ workspaceInitialised(): Promise<boolean>;
204
582
  /**
205
583
  * Initialise a kanbn board in the current working directory
206
584
  * @param {object} [options={}] Initial columns and other config options
@@ -265,14 +643,14 @@ export class Kanbn {
265
643
  * @param {boolean} [relative=false] Treat the position argument as relative instead of absolute
266
644
  * @return {Promise<string>} The id of the task that was moved
267
645
  */
268
- moveTask(taskId: string, columnName: string, position?: number | null, relative?: boolean): Promise<string>;
646
+ moveTask(taskId: string, columnName: string, position?: number | null, relative?: boolean, add?: boolean): Promise<string>;
269
647
  /**
270
648
  * Remove a task from the index and optionally delete the task file as well
271
649
  * @param {string} taskId The id of the task to remove
272
650
  * @param {boolean} [removeFile=false] True if the task file should be removed
273
651
  * @return {Promise<string>} The id of the task that was deleted
274
652
  */
275
- deleteTask(taskId: string, removeFile?: boolean): Promise<string>;
653
+ deleteTask(taskId: string, removeFile?: boolean, allBoards?: boolean): Promise<string>;
276
654
  /**
277
655
  * Search for indexed tasks
278
656
  * @param {object} [filters={}] The filters to apply
@@ -329,7 +707,7 @@ export class Kanbn {
329
707
  * @param {string} author The comment author
330
708
  * @return {Promise<string>} The task id
331
709
  */
332
- comment(taskId: string, text: string, author: string): Promise<string>;
710
+ comment(taskId: string, text: string, author?: string): Promise<string>;
333
711
  /**
334
712
  * Return a list of archived tasks
335
713
  * @return {Promise<string[]>} A list of archived task ids
@@ -347,7 +725,7 @@ export class Kanbn {
347
725
  * @param {?string} [columnName=null] The column to restore the task to
348
726
  * @return {Promise<string>} The task id
349
727
  */
350
- restoreTask(taskId: string, columnName?: string | null): Promise<string>;
728
+ restoreTask(taskId: string, columnName?: string | null, singleBoard?: boolean): Promise<string>;
351
729
  /**
352
730
  * Nuke it from orbit, it's the only way to be sure
353
731
  */