@basementuniverse/kanbn 2.0.0 → 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 (87) hide show
  1. package/README.md +2 -1
  2. package/docs/advanced-configuration.md +23 -0
  3. package/docs/commands/add.txt +9 -0
  4. package/docs/commands/archive.txt +5 -0
  5. package/docs/commands/board.txt +5 -0
  6. package/docs/commands/boards.txt +34 -0
  7. package/docs/commands/burndown.txt +6 -0
  8. package/docs/commands/comment.txt +5 -0
  9. package/docs/commands/edit.txt +5 -0
  10. package/docs/commands/find.txt +11 -0
  11. package/docs/commands/gantt.txt +5 -0
  12. package/docs/commands/help.txt +1 -0
  13. package/docs/commands/history.txt +5 -0
  14. package/docs/commands/init.txt +13 -0
  15. package/docs/commands/move.txt +9 -0
  16. package/docs/commands/remove.txt +12 -1
  17. package/docs/commands/rename.txt +5 -0
  18. package/docs/commands/restore.txt +6 -0
  19. package/docs/commands/sort.txt +5 -0
  20. package/docs/commands/sprint.txt +9 -0
  21. package/docs/commands/status.txt +10 -1
  22. package/docs/commands/task.txt +5 -0
  23. package/docs/commands/validate.txt +15 -0
  24. package/docs/index-structure.md +26 -0
  25. package/docs/index.md +3 -1
  26. package/docs/multiple-boards.md +258 -0
  27. package/docs/quick-start.md +21 -1
  28. package/docs/task-structure.md +22 -1
  29. package/example/README.md +23 -0
  30. package/example/boards/.kanbn/design.md +31 -0
  31. package/example/boards/.kanbn/index.md +44 -0
  32. package/example/boards/.kanbn/tasks/add-usage-alert-emails.md +19 -0
  33. package/example/boards/.kanbn/tasks/build-tenant-settings-page.md +43 -0
  34. package/example/boards/.kanbn/tasks/create-organization-switcher.md +44 -0
  35. package/example/boards/.kanbn/tasks/design-onboarding-checklist.md +22 -0
  36. package/example/boards/.kanbn/tasks/refresh-marketing-site.md +28 -0
  37. package/example/boards/.kanbn/tasks/ship-billing-portal.md +29 -0
  38. package/package.json +9 -7
  39. package/routes/add.json +35 -11
  40. package/routes/archive.json +10 -2
  41. package/routes/board.json +11 -3
  42. package/routes/boards.json +30 -0
  43. package/routes/burndown.json +23 -7
  44. package/routes/comment.json +14 -4
  45. package/routes/edit.json +32 -10
  46. package/routes/find.json +37 -12
  47. package/routes/gantt.json +20 -6
  48. package/routes/history.json +39 -25
  49. package/routes/init.json +3 -1
  50. package/routes/move.json +22 -6
  51. package/routes/remove.json +15 -4
  52. package/routes/rename.json +11 -3
  53. package/routes/restore.json +8 -2
  54. package/routes/sort.json +35 -11
  55. package/routes/sprint.json +14 -4
  56. package/routes/status.json +23 -7
  57. package/routes/task.json +10 -2
  58. package/routes/validate.json +18 -5
  59. package/skills/kanbn-plan/SKILL.md +10 -1
  60. package/skills/kanbn-replan/SKILL.md +6 -1
  61. package/src/board.js +1 -1
  62. package/src/controller/add.js +52 -46
  63. package/src/controller/archive.js +8 -4
  64. package/src/controller/board.js +8 -9
  65. package/src/controller/boards.js +140 -0
  66. package/src/controller/burndown.js +9 -5
  67. package/src/controller/comment.js +9 -5
  68. package/src/controller/edit.js +9 -5
  69. package/src/controller/find.js +12 -9
  70. package/src/controller/gantt.js +8 -4
  71. package/src/controller/history.js +8 -4
  72. package/src/controller/init.js +39 -4
  73. package/src/controller/move.js +69 -15
  74. package/src/controller/remove.js +34 -11
  75. package/src/controller/rename.js +8 -4
  76. package/src/controller/restore.js +23 -8
  77. package/src/controller/sort.js +19 -3
  78. package/src/controller/sprint.js +31 -7
  79. package/src/controller/status.js +8 -4
  80. package/src/controller/task.js +22 -9
  81. package/src/controller/validate.js +60 -7
  82. package/src/git-user-name.js +19 -0
  83. package/src/main.d.ts +184 -6
  84. package/src/main.js +1333 -63
  85. package/src/parse-index.js +14 -0
  86. package/src/parse-task.js +16 -0
  87. package/src/utility.js +140 -0
@@ -1,7 +1,10 @@
1
- const kanbn = require('../main');
1
+ const workspace = require('../main');
2
2
  const utility = require('../utility');
3
3
  const yaml = require('yamljs');
4
4
 
5
+ // The board this command targets - set from --board before anything else runs
6
+ let kanbn = workspace;
7
+
5
8
  /**
6
9
  * Show a list of tasks whose dates disagree with the column they're in
7
10
  * @param {object[]} drift
@@ -27,18 +30,36 @@ function showDrift(drift, json) {
27
30
  }
28
31
  }
29
32
 
33
+ /**
34
+ * Show warnings about a multi-board workspace
35
+ * @param {object[]} warnings
36
+ */
37
+ function showBoardWarnings(warnings) {
38
+ if (!warnings.length) {
39
+ return;
40
+ }
41
+ console.log(
42
+ `${warnings.length} ${warnings.length === 1 ? 'warning' : 'warnings'}:`
43
+ );
44
+ for (const warning of warnings) {
45
+ console.log(` ${warning.board === null ? 'workspace' : warning.board}: ${warning.message}`);
46
+ }
47
+ }
48
+
30
49
  module.exports = async args => {
31
50
 
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}');
51
+ // Work out which board this command targets
52
+ const scoped = await utility.resolveBoard(workspace, args);
53
+ if (scoped === null) {
35
54
  return;
36
55
  }
56
+ kanbn = scoped;
37
57
 
38
- // Validate kanbn files
58
+ // Validate kanbn files, on this board or on every board in the workspace
59
+ const allBoards = !!args['all-boards'];
39
60
  let result;
40
61
  try {
41
- result = await kanbn.validate(args.save);
62
+ result = allBoards ? await kanbn.validateAllBoards(args.save) : await kanbn.validate(args.save);
42
63
  } catch (error) {
43
64
  utility.error(error);
44
65
  return;
@@ -54,6 +75,20 @@ module.exports = async args => {
54
75
  return;
55
76
  }
56
77
 
78
+ // Report multi-board problems. These are warnings rather than errors: each one describes a
79
+ // workspace that still works, just not the way its author probably meant it to. A workspace with
80
+ // one board has none of these problems to have, and its output is exactly what it always was
81
+ const multiBoard = (await kanbn.listBoards()).length > 1;
82
+ let boardWarnings = [];
83
+ if (multiBoard) {
84
+ try {
85
+ boardWarnings = await kanbn.findBoardWarnings();
86
+ } catch (error) {
87
+ utility.error(error);
88
+ return;
89
+ }
90
+ }
91
+
57
92
  // Backfill missing started and completed dates
58
93
  if (args.fix) {
59
94
  let fixed;
@@ -86,8 +121,26 @@ module.exports = async args => {
86
121
  utility.error(error);
87
122
  return;
88
123
  }
124
+ if (args.json) {
125
+
126
+ // A multi-board workspace has warnings to report as well as drift, so JSON output carries both.
127
+ // With a single board it stays the bare drift list it has always been
128
+ if (multiBoard) {
129
+ console.log(JSON.stringify({ drift, warnings: boardWarnings }, null, 2));
130
+ return;
131
+ }
132
+ if (drift.length) {
133
+ showDrift(drift, true);
134
+ return;
135
+ }
136
+ console.log('Everything OK');
137
+ return;
138
+ }
89
139
  if (drift.length) {
90
- showDrift(drift, args.json);
140
+ showDrift(drift, false);
141
+ }
142
+ showBoardWarnings(boardWarnings);
143
+ if (drift.length || boardWarnings.length) {
91
144
  return;
92
145
  }
93
146
  console.log('Everything OK');
@@ -0,0 +1,19 @@
1
+ const { execFileSync } = require('child_process');
2
+
3
+ /**
4
+ * Get the git username of the current user, or null if it can't be found. This replaces the
5
+ * git-user-name package, which depends on a version of parse-git-config with an unpatched
6
+ * prototype pollution vulnerability
7
+ * @return {string|null} The git username, or null if git isn't available or no username is set
8
+ */
9
+ module.exports = () => {
10
+ try {
11
+ const username = execFileSync('git', ['config', 'user.name'], {
12
+ encoding: 'utf8',
13
+ stdio: ['ignore', 'pipe', 'ignore']
14
+ }).trim();
15
+ return username || null;
16
+ } catch (error) {
17
+ return null;
18
+ }
19
+ };
package/src/main.d.ts CHANGED
@@ -4,7 +4,29 @@ 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
8
30
  };
9
31
 
10
32
  declare type index = {
@@ -59,11 +81,24 @@ declare type sprint = {
59
81
  };
60
82
 
61
83
  export class Kanbn {
62
- constructor(root?: any);
84
+ constructor(root?: any, options?: { board?: string, caches?: any });
63
85
  ROOT: string;
64
86
  CONFIG_YAML: string;
65
87
  CONFIG_JSON: string;
66
88
  configMemo: any;
89
+ /**
90
+ * The board this instance is scoped to, or null for the main board
91
+ */
92
+ boardSlug: string | null;
93
+ /**
94
+ * Get a copy of this instance scoped to another board, sharing this one's cached config
95
+ * @param {?string} [slug=null] The board slug, or null/"main"/"default" for the main board
96
+ */
97
+ board(slug?: string | null): Kanbn;
98
+ /**
99
+ * Alias for board()
100
+ */
101
+ withBoard(slug?: string | null): Kanbn;
67
102
  /**
68
103
  * Check if a separate config file exists
69
104
  * @returns {Promise<boolean>} True if a config file exists
@@ -108,7 +143,25 @@ export class Kanbn {
108
143
  */
109
144
  getMainFolder(): Promise<string>;
110
145
  /**
111
- * Get the index path
146
+ * Get the main board's slug, i.e. the index file name without its extension
147
+ * @return {Promise<string>} The main board slug
148
+ */
149
+ getMainBoardSlug(): Promise<string>;
150
+ /**
151
+ * Resolve a board slug, mapping the reserved aliases and an absent slug onto the main board
152
+ * @param {?string} [slug] The board slug, defaulting to this instance's board
153
+ */
154
+ resolveBoardSlug(slug?: string | null): Promise<string>;
155
+ /**
156
+ * Check if a slug refers to the main board
157
+ */
158
+ isMainBoard(slug?: string | null): Promise<boolean>;
159
+ /**
160
+ * Get the file path for a board
161
+ */
162
+ getBoardPath(slug?: string | null): Promise<string>;
163
+ /**
164
+ * Get the index path, i.e. the path of the board this instance is scoped to
112
165
  * @return {Promise<string>} The kanbn index path
113
166
  */
114
167
  getIndexPath(): Promise<string>;
@@ -154,6 +207,126 @@ export class Kanbn {
154
207
  * @param {object} indexData Index data to save
155
208
  */
156
209
  saveIndex(indexData: object): Promise<void>;
210
+ /**
211
+ * Get the workspace-scoped options, from the config file or the main board's front matter
212
+ */
213
+ getWorkspaceOptions(): Promise<Record<string, any>>;
214
+ /**
215
+ * Load the workspace options along with where they came from
216
+ */
217
+ loadWorkspaceOptions(): Promise<{ options: Record<string, any>, fromConfig: boolean }>;
218
+ /**
219
+ * Get the options a secondary board inherits from the workspace
220
+ */
221
+ getInheritedBoardOptions(): Promise<Record<string, any>>;
222
+ /**
223
+ * Get the per-board options declared for a board under the `boards` key in the config file
224
+ */
225
+ getBoardConfig(slug?: string | null): Promise<Record<string, any>>;
226
+ /**
227
+ * Layer a board's own front matter options over the workspace options
228
+ */
229
+ resolveBoardOptions(resolvedSlug: string, ownOptions: Record<string, any>): Promise<Record<string, any>>;
230
+ /**
231
+ * Work out which options belong in a secondary board's own front matter
232
+ */
233
+ getOwnBoardOptions(resolvedSlug: string, indexData: object): Promise<Record<string, any>>;
234
+ /**
235
+ * Overwrite a board file with the specified data
236
+ */
237
+ saveBoard(slug: string | null, indexData: object): Promise<void>;
238
+ /**
239
+ * Load a board file and parse it to an object
240
+ */
241
+ loadBoard(slug?: string | null): Promise<index>;
242
+ /**
243
+ * Work out which board a command should target: --board, then KANBN_BOARD, then defaultBoard
244
+ */
245
+ resolveTargetBoard(slug?: string | null): Promise<string | null>;
246
+ /**
247
+ * Get a Kanbn instance scoped to the board a command's arguments point at
248
+ */
249
+ boardFromArgs(args?: object): Promise<Kanbn>;
250
+ /**
251
+ * Find all boards in the workspace
252
+ */
253
+ listBoards(): Promise<board[]>;
254
+ /**
255
+ * List boards with column and task counts, completion percentage and last modified date
256
+ */
257
+ getBoardsSummary(): Promise<boardSummary[]>;
258
+ /**
259
+ * Get every task that appears on more than one board, with the column it occupies on each
260
+ */
261
+ getCrossBoardTasks(allTasks?: boolean): Promise<{ id: string, boards: Record<string, string> }[]>;
262
+ /**
263
+ * Check if a board exists
264
+ */
265
+ boardExists(slug: string): Promise<boolean>;
266
+ /**
267
+ * Get the value to record in a history event's `board` key, or null for the main board
268
+ */
269
+ historyBoard(): Promise<string | null>;
270
+ /**
271
+ * Add an existing task to this board
272
+ */
273
+ addTaskToBoard(taskId: string, columnName: string): Promise<string>;
274
+ /**
275
+ * Check if a task file exists, regardless of whether any board references it
276
+ */
277
+ taskFileExists(taskId: string): Promise<boolean>;
278
+ /**
279
+ * Boards named in an archived task's metadata that no longer existed when it was restored
280
+ */
281
+ lastRestoreWarnings: string[];
282
+ /**
283
+ * Create a new board
284
+ */
285
+ createBoard(slug: string, options?: object): Promise<string>;
286
+ /**
287
+ * Create a secondary board, or update an existing one
288
+ */
289
+ initialiseBoard(slug: string, options?: object): Promise<string>;
290
+ /**
291
+ * Delete a board file, returning the ids of tasks that are no longer on any board
292
+ */
293
+ deleteBoard(slug: string): Promise<string[]>;
294
+ /**
295
+ * Find the tasks that would become untracked if a board were deleted
296
+ */
297
+ findOrphanedTasks(slug: string): Promise<string[]>;
298
+ /**
299
+ * Rename a board
300
+ */
301
+ renameBoard(slug: string, newSlug: string, newName?: string | null): Promise<string>;
302
+ /**
303
+ * Find every board that references a task, and the column it occupies on each
304
+ */
305
+ findTaskBoards(taskId: string): Promise<Record<string, string>>;
306
+ /**
307
+ * Alias for findTaskBoards()
308
+ */
309
+ getTaskBoardColumns(taskId: string): Promise<Record<string, string>>;
310
+ /**
311
+ * Get the slugs that can't be used for a board
312
+ */
313
+ getReservedBoardSlugs(): Promise<string[]>;
314
+ /**
315
+ * Check that a slug can be used for a new board, throwing if it can't
316
+ */
317
+ validateBoardSlug(slug: string): Promise<string>;
318
+ /**
319
+ * Get the boards config from the config file, i.e. the exclude list and display order
320
+ */
321
+ getBoardsConfig(): Promise<{ exclude: string[], order: string[] }>;
322
+ /**
323
+ * Find tasks that no board references at all
324
+ */
325
+ findWorkspaceUntrackedTasks(): Promise<Set<string>>;
326
+ /**
327
+ * Find tasks that other boards track but this one doesn't
328
+ */
329
+ findTasksOnOtherBoards(): Promise<Record<string, Record<string, string>>>;
157
330
  /**
158
331
  * Load the index file and parse it to an object
159
332
  * @return {Promise<object>} The index object
@@ -201,6 +374,11 @@ export class Kanbn {
201
374
  * @return {Promise<boolean>} True if the current working directory has been initialised, otherwise false
202
375
  */
203
376
  initialised(): Promise<boolean>;
377
+ /**
378
+ * Check if the workspace has been initialised, regardless of which board this instance is scoped to
379
+ * @return {Promise<boolean>} True if the main board exists
380
+ */
381
+ workspaceInitialised(): Promise<boolean>;
204
382
  /**
205
383
  * Initialise a kanbn board in the current working directory
206
384
  * @param {object} [options={}] Initial columns and other config options
@@ -265,14 +443,14 @@ export class Kanbn {
265
443
  * @param {boolean} [relative=false] Treat the position argument as relative instead of absolute
266
444
  * @return {Promise<string>} The id of the task that was moved
267
445
  */
268
- moveTask(taskId: string, columnName: string, position?: number | null, relative?: boolean): Promise<string>;
446
+ moveTask(taskId: string, columnName: string, position?: number | null, relative?: boolean, add?: boolean): Promise<string>;
269
447
  /**
270
448
  * Remove a task from the index and optionally delete the task file as well
271
449
  * @param {string} taskId The id of the task to remove
272
450
  * @param {boolean} [removeFile=false] True if the task file should be removed
273
451
  * @return {Promise<string>} The id of the task that was deleted
274
452
  */
275
- deleteTask(taskId: string, removeFile?: boolean): Promise<string>;
453
+ deleteTask(taskId: string, removeFile?: boolean, allBoards?: boolean): Promise<string>;
276
454
  /**
277
455
  * Search for indexed tasks
278
456
  * @param {object} [filters={}] The filters to apply
@@ -347,7 +525,7 @@ export class Kanbn {
347
525
  * @param {?string} [columnName=null] The column to restore the task to
348
526
  * @return {Promise<string>} The task id
349
527
  */
350
- restoreTask(taskId: string, columnName?: string | null): Promise<string>;
528
+ restoreTask(taskId: string, columnName?: string | null, singleBoard?: boolean): Promise<string>;
351
529
  /**
352
530
  * Nuke it from orbit, it's the only way to be sure
353
531
  */