@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.
- package/README.md +2 -1
- package/docs/advanced-configuration.md +23 -0
- package/docs/commands/add.txt +9 -0
- package/docs/commands/archive.txt +5 -0
- package/docs/commands/board.txt +5 -0
- package/docs/commands/boards.txt +34 -0
- package/docs/commands/burndown.txt +6 -0
- package/docs/commands/comment.txt +5 -0
- package/docs/commands/edit.txt +5 -0
- package/docs/commands/find.txt +11 -0
- package/docs/commands/gantt.txt +5 -0
- package/docs/commands/help.txt +1 -0
- package/docs/commands/history.txt +5 -0
- package/docs/commands/init.txt +13 -0
- package/docs/commands/move.txt +9 -0
- package/docs/commands/remove.txt +12 -1
- package/docs/commands/rename.txt +5 -0
- package/docs/commands/restore.txt +6 -0
- package/docs/commands/sort.txt +5 -0
- package/docs/commands/sprint.txt +9 -0
- package/docs/commands/status.txt +10 -1
- package/docs/commands/task.txt +5 -0
- package/docs/commands/validate.txt +15 -0
- package/docs/index-structure.md +26 -0
- package/docs/index.md +3 -1
- package/docs/multiple-boards.md +258 -0
- package/docs/quick-start.md +21 -1
- package/docs/task-structure.md +22 -1
- package/example/README.md +23 -0
- package/example/boards/.kanbn/design.md +31 -0
- package/example/boards/.kanbn/index.md +44 -0
- package/example/boards/.kanbn/tasks/add-usage-alert-emails.md +19 -0
- package/example/boards/.kanbn/tasks/build-tenant-settings-page.md +43 -0
- package/example/boards/.kanbn/tasks/create-organization-switcher.md +44 -0
- package/example/boards/.kanbn/tasks/design-onboarding-checklist.md +22 -0
- package/example/boards/.kanbn/tasks/refresh-marketing-site.md +28 -0
- package/example/boards/.kanbn/tasks/ship-billing-portal.md +29 -0
- package/package.json +9 -7
- package/routes/add.json +35 -11
- package/routes/archive.json +10 -2
- package/routes/board.json +11 -3
- package/routes/boards.json +30 -0
- package/routes/burndown.json +23 -7
- package/routes/comment.json +14 -4
- package/routes/edit.json +32 -10
- package/routes/find.json +37 -12
- package/routes/gantt.json +20 -6
- package/routes/history.json +39 -25
- package/routes/init.json +3 -1
- package/routes/move.json +22 -6
- package/routes/remove.json +15 -4
- package/routes/rename.json +11 -3
- package/routes/restore.json +8 -2
- package/routes/sort.json +35 -11
- package/routes/sprint.json +14 -4
- package/routes/status.json +23 -7
- package/routes/task.json +10 -2
- package/routes/validate.json +18 -5
- package/skills/kanbn-plan/SKILL.md +10 -1
- package/skills/kanbn-replan/SKILL.md +6 -1
- package/src/board.js +1 -1
- package/src/controller/add.js +52 -46
- package/src/controller/archive.js +8 -4
- package/src/controller/board.js +8 -9
- package/src/controller/boards.js +140 -0
- package/src/controller/burndown.js +9 -5
- package/src/controller/comment.js +9 -5
- package/src/controller/edit.js +9 -5
- package/src/controller/find.js +12 -9
- package/src/controller/gantt.js +8 -4
- package/src/controller/history.js +8 -4
- package/src/controller/init.js +39 -4
- package/src/controller/move.js +69 -15
- package/src/controller/remove.js +34 -11
- package/src/controller/rename.js +8 -4
- package/src/controller/restore.js +23 -8
- package/src/controller/sort.js +19 -3
- package/src/controller/sprint.js +31 -7
- package/src/controller/status.js +8 -4
- package/src/controller/task.js +22 -9
- package/src/controller/validate.js +60 -7
- package/src/git-user-name.js +19 -0
- package/src/main.d.ts +184 -6
- package/src/main.js +1333 -63
- package/src/parse-index.js +14 -0
- package/src/parse-task.js +16 -0
- package/src/utility.js +140 -0
package/src/parse-index.js
CHANGED
|
@@ -27,6 +27,20 @@ function validateOptions(options) {
|
|
|
27
27
|
'startedField': { type: 'string' },
|
|
28
28
|
'completedField': { type: 'string' },
|
|
29
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
|
+
},
|
|
30
44
|
'sprints': {
|
|
31
45
|
type: 'array',
|
|
32
46
|
items: {
|
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' }
|
|
@@ -259,6 +261,13 @@ function validateHistoryEvent(historyEvent) {
|
|
|
259
261
|
throw new Error('created history event is missing column');
|
|
260
262
|
}
|
|
261
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;
|
|
262
271
|
case 'moved':
|
|
263
272
|
if (!historyEvent.fromColumn || !historyEvent.toColumn) {
|
|
264
273
|
throw new Error('moved history event is missing fromColumn or toColumn');
|
|
@@ -293,6 +302,7 @@ const KNOWN_HISTORY_EVENT_KEYS = [
|
|
|
293
302
|
'column',
|
|
294
303
|
'fromColumn',
|
|
295
304
|
'toColumn',
|
|
305
|
+
'board',
|
|
296
306
|
'fromProgress',
|
|
297
307
|
'toProgress',
|
|
298
308
|
'author'
|
|
@@ -670,6 +680,12 @@ module.exports = {
|
|
|
670
680
|
if ('toColumn' in historyEvent) {
|
|
671
681
|
historyEventOutput.push(`toColumn: ${historyEvent.toColumn}`);
|
|
672
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
|
+
}
|
|
673
689
|
if ('fromProgress' in historyEvent) {
|
|
674
690
|
historyEventOutput.push(`fromProgress: ${historyEvent.fromProgress}`);
|
|
675
691
|
}
|
package/src/utility.js
CHANGED
|
@@ -19,6 +19,135 @@ 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
|
+
|
|
22
151
|
/**
|
|
23
152
|
* Show a hint about tasks whose dates don't match the column they're in. Hints are suppressed
|
|
24
153
|
* for machine-readable output and when the board sets verbose to false
|
|
@@ -166,6 +295,17 @@ module.exports = (() => {
|
|
|
166
295
|
return s;
|
|
167
296
|
},
|
|
168
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
|
+
|
|
169
309
|
/**
|
|
170
310
|
* Zip 2 arrays together, i.e. ([1, 2, 3], [a, b, c]) => [[1, a], [2, b], [3, c]]
|
|
171
311
|
* @param {any[]} a
|