@basementuniverse/kanbn 2.1.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.
- package/README.md +1 -0
- package/coverage/tmp/coverage-916017-1788028598821-0.json +1 -0
- package/coverage/tmp/{coverage-214293-1787777184569-0.json → coverage-916018-1788028597179-0.json} +1 -1
- package/coverage/tmp/coverage-916036-1788028598796-0.json +1 -0
- package/docs/actions.md +337 -0
- package/docs/advanced-configuration.md +32 -0
- package/docs/commands/add.txt +8 -1
- package/docs/commands/archive.txt +6 -0
- package/docs/commands/board.txt +4 -0
- package/docs/commands/burndown.txt +1 -0
- package/docs/commands/comment.txt +8 -1
- package/docs/commands/contributors.txt +58 -0
- package/docs/commands/edit.txt +13 -1
- package/docs/commands/find.txt +28 -1
- package/docs/commands/gantt.txt +1 -0
- package/docs/commands/help.txt +1 -0
- package/docs/commands/history.txt +1 -0
- package/docs/commands/move.txt +17 -0
- package/docs/commands/remove.txt +10 -0
- package/docs/commands/restore.txt +6 -0
- package/docs/commands/sort.txt +18 -0
- package/docs/commands/task.txt +4 -0
- package/docs/commands/validate.txt +14 -1
- package/docs/contributors.md +145 -0
- package/docs/filtering-and-sorting.md +60 -3
- package/docs/index-structure.md +119 -11
- package/docs/index.md +3 -1
- package/docs/multiple-boards.md +1 -0
- package/docs/task-structure.md +9 -2
- package/example/advanced/kanbn.yml +65 -0
- package/package.json +1 -1
- package/routes/add.json +5 -1
- package/routes/archive.json +6 -2
- package/routes/comment.json +5 -1
- package/routes/contributors.json +18 -0
- package/routes/edit.json +5 -1
- package/routes/move.json +4 -2
- package/routes/remove.json +6 -2
- package/routes/restore.json +6 -0
- package/routes/sort.json +5 -0
- package/src/actions.js +904 -0
- package/src/board.js +24 -1
- package/src/controller/add.js +21 -10
- package/src/controller/archive.js +1 -0
- package/src/controller/board.js +13 -4
- package/src/controller/burndown.js +5 -2
- package/src/controller/comment.js +5 -2
- package/src/controller/contributors.js +166 -0
- package/src/controller/edit.js +40 -14
- package/src/controller/find.js +75 -4
- package/src/controller/gantt.js +5 -2
- package/src/controller/history.js +5 -2
- package/src/controller/move.js +84 -10
- package/src/controller/remove.js +39 -2
- package/src/controller/restore.js +1 -0
- package/src/controller/sort.js +40 -0
- package/src/controller/task.js +12 -1
- package/src/controller/validate.js +141 -4
- package/src/git-user-name.js +5 -15
- package/src/git-user.js +55 -0
- package/src/main.d.ts +204 -4
- package/src/main.js +1412 -38
- package/src/parse-index.js +205 -17
- package/src/utility.js +135 -1
- package/coverage/tmp/coverage-214292-1787777191526-0.json +0 -1
package/src/parse-index.js
CHANGED
|
@@ -112,6 +112,66 @@ function validateOptions(options) {
|
|
|
112
112
|
required: ['name', 'type']
|
|
113
113
|
}
|
|
114
114
|
},
|
|
115
|
+
'contributors': {
|
|
116
|
+
type: 'array',
|
|
117
|
+
items: {
|
|
118
|
+
oneOf: [
|
|
119
|
+
|
|
120
|
+
// A bare name, which is all most workspaces ever need
|
|
121
|
+
{ type: 'string' },
|
|
122
|
+
{
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
'name': { type: 'string' },
|
|
126
|
+
'displayName': { type: 'string' },
|
|
127
|
+
'email': { type: 'string' },
|
|
128
|
+
'aliases': {
|
|
129
|
+
type: 'array',
|
|
130
|
+
items: { type: 'string' }
|
|
131
|
+
},
|
|
132
|
+
'colour': { type: 'string' }
|
|
133
|
+
},
|
|
134
|
+
required: ['name']
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
'actionsFile': { type: 'string' },
|
|
140
|
+
|
|
141
|
+
// The rules themselves are only shape-checked here: what they mean is checked by
|
|
142
|
+
// actions.findRuleErrors(), which knows the event names, the verbs and this board's columns and
|
|
143
|
+
// can say what is wrong rather than which sub-schema failed
|
|
144
|
+
'actions': {
|
|
145
|
+
type: 'array',
|
|
146
|
+
items: {
|
|
147
|
+
type: 'object',
|
|
148
|
+
properties: {
|
|
149
|
+
'name': { type: 'string' },
|
|
150
|
+
'on': { type: 'string' },
|
|
151
|
+
'when': { type: 'object' },
|
|
152
|
+
'for': {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
'related': { type: 'string' },
|
|
156
|
+
'direction': {
|
|
157
|
+
type: 'string',
|
|
158
|
+
enum: [
|
|
159
|
+
'incoming',
|
|
160
|
+
'outgoing'
|
|
161
|
+
]
|
|
162
|
+
},
|
|
163
|
+
'where': { type: 'object' }
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
'anyBoard': { type: 'boolean' },
|
|
167
|
+
'then': {
|
|
168
|
+
type: 'array',
|
|
169
|
+
items: { type: 'object' }
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
required: ['on', 'then']
|
|
173
|
+
}
|
|
174
|
+
},
|
|
115
175
|
'views': {
|
|
116
176
|
type: 'array',
|
|
117
177
|
items: {
|
|
@@ -171,6 +231,126 @@ function validateOptions(options) {
|
|
|
171
231
|
}
|
|
172
232
|
}
|
|
173
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Get the task id from a column list item, or null if the item isn't a task link
|
|
236
|
+
*
|
|
237
|
+
* A task link is a list item that begins with a markdown link, e.g. `- [task-id](tasks/task-id.md)`.
|
|
238
|
+
* Anything else - a bare title, a checkbox item, a note - is content that this file preserves but
|
|
239
|
+
* doesn't interpret
|
|
240
|
+
* @param {object} item A list item token
|
|
241
|
+
* @return {?string} The task id, or null if this item isn't a task link
|
|
242
|
+
*/
|
|
243
|
+
function taskIdFromListItem(item) {
|
|
244
|
+
const block = item.tokens && item.tokens[0];
|
|
245
|
+
const inline = block && block.tokens && block.tokens[0];
|
|
246
|
+
return inline && inline.type === 'link' ? inline.text : null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Remove trailing whitespace from a markdown token's raw source, keeping any internal line breaks so
|
|
251
|
+
* that multi-line items survive a read/write cycle intact
|
|
252
|
+
* @param {string} raw
|
|
253
|
+
* @return {string}
|
|
254
|
+
*/
|
|
255
|
+
function trimRaw(raw) {
|
|
256
|
+
return String(raw).replace(/\s+$/, '');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Split a column's markdown into the task ids it references and everything else it contains
|
|
261
|
+
*
|
|
262
|
+
* Every top-level token is considered, not just the first list: a single line using a different
|
|
263
|
+
* bullet character splits one list into several, and reading only the first one silently dropped
|
|
264
|
+
* every task after it. Content that isn't a task link is recorded with the position it occupied so
|
|
265
|
+
* that it can be written back where the user put it
|
|
266
|
+
* @param {string} markdown The column's markdown content
|
|
267
|
+
* @return {{taskIds: string[], content: object[]}}
|
|
268
|
+
*/
|
|
269
|
+
function parseColumnContent(markdown) {
|
|
270
|
+
const taskIds = [], content = [];
|
|
271
|
+
if (!markdown) {
|
|
272
|
+
return { taskIds, content };
|
|
273
|
+
}
|
|
274
|
+
let position = 0;
|
|
275
|
+
for (const token of marked.lexer(markdown)) {
|
|
276
|
+
if (token.type === 'space') {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (token.type === 'list') {
|
|
280
|
+
for (const item of token.items) {
|
|
281
|
+
const taskId = taskIdFromListItem(item);
|
|
282
|
+
if (taskId === null) {
|
|
283
|
+
content.push({ position, text: item.text.trim(), raw: trimRaw(item.raw) });
|
|
284
|
+
} else {
|
|
285
|
+
taskIds.push(taskId);
|
|
286
|
+
}
|
|
287
|
+
position++;
|
|
288
|
+
}
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// A paragraph, quote, code block or anything else that isn't a list is content as well. It's
|
|
293
|
+
// written back surrounded by blank lines so that it doesn't merge into a neighbouring list
|
|
294
|
+
content.push({
|
|
295
|
+
position,
|
|
296
|
+
text: ('text' in token ? String(token.text) : trimRaw(token.raw)).trim(),
|
|
297
|
+
raw: trimRaw(token.raw),
|
|
298
|
+
block: true
|
|
299
|
+
});
|
|
300
|
+
position++;
|
|
301
|
+
}
|
|
302
|
+
return { taskIds, content };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Rebuild a column's markdown from its task ids and preserved content
|
|
307
|
+
* @param {string[]} taskIds
|
|
308
|
+
* @param {object[]} content
|
|
309
|
+
* @return {string} The column's markdown content
|
|
310
|
+
*/
|
|
311
|
+
function buildColumnContent(taskIds, content) {
|
|
312
|
+
const lines = taskIds.map(taskId => `- [${taskId}](tasks/${taskId}.md)`);
|
|
313
|
+
|
|
314
|
+
// Positions were recorded against the column as it was read. Tasks move, so a position can end up
|
|
315
|
+
// past the end of the column - clamping keeps the line in the column it was written in
|
|
316
|
+
for (const entry of [...content].sort((a, b) => a.position - b.position)) {
|
|
317
|
+
lines.splice(
|
|
318
|
+
Math.min(entry.position, lines.length),
|
|
319
|
+
0,
|
|
320
|
+
entry.block ? `\n${entry.raw}\n` : entry.raw
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Validate the preserved column content object
|
|
328
|
+
* @param {object} columnContent
|
|
329
|
+
*/
|
|
330
|
+
function validateColumnContent(columnContent) {
|
|
331
|
+
const result = validate(columnContent, {
|
|
332
|
+
type: 'object',
|
|
333
|
+
patternProperties: {
|
|
334
|
+
'^[\w ]+$': {
|
|
335
|
+
type: 'array',
|
|
336
|
+
items: {
|
|
337
|
+
type: 'object',
|
|
338
|
+
properties: {
|
|
339
|
+
'position': { type: 'number' },
|
|
340
|
+
'text': { type: 'string' },
|
|
341
|
+
'raw': { type: 'string' },
|
|
342
|
+
'block': { type: 'boolean' }
|
|
343
|
+
},
|
|
344
|
+
required: ['position', 'raw']
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
if (result.errors.length) {
|
|
350
|
+
throw new Error(result.errors.map(error => `${error.property} ${error.message}`).join('\n'));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
174
354
|
/**
|
|
175
355
|
* Validate the columns object
|
|
176
356
|
* @param {object} columns
|
|
@@ -198,7 +378,7 @@ module.exports = {
|
|
|
198
378
|
* @return {object}
|
|
199
379
|
*/
|
|
200
380
|
md2json(data) {
|
|
201
|
-
let name = '', description = '', options = {}, columns = {};
|
|
381
|
+
let name = '', description = '', options = {}, columns = {}, columnContent = {};
|
|
202
382
|
try {
|
|
203
383
|
|
|
204
384
|
// Check data type
|
|
@@ -256,26 +436,32 @@ module.exports = {
|
|
|
256
436
|
|
|
257
437
|
// Parse columns
|
|
258
438
|
const columnNames = Object.keys(index).filter(column => ['raw', 'Options', name].indexOf(column) === -1);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
439
|
+
for (const columnName of columnNames) {
|
|
440
|
+
let parsed = null;
|
|
441
|
+
try {
|
|
442
|
+
parsed = parseColumnContent(index[columnName].content);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
throw new Error(`unable to parse column "${columnName}" (${error.message})`);
|
|
445
|
+
}
|
|
446
|
+
columns[columnName] = parsed.taskIds;
|
|
447
|
+
|
|
448
|
+
// Anything in the column that isn't a task link is kept verbatim rather than rejected, so
|
|
449
|
+
// that a hand-written note or a mistyped link is preserved instead of being thrown away the
|
|
450
|
+
// next time the board is saved. Nothing else in kanbn reads it
|
|
451
|
+
if (parsed.content.length) {
|
|
452
|
+
columnContent[columnName] = parsed.content;
|
|
453
|
+
}
|
|
272
454
|
}
|
|
273
455
|
} catch (error) {
|
|
274
456
|
throw new Error(`Unable to parse index: ${error.message}`);
|
|
275
457
|
}
|
|
276
458
|
|
|
277
459
|
// Assemble index object
|
|
278
|
-
|
|
460
|
+
const result = { name, description, options, columns };
|
|
461
|
+
if (Object.keys(columnContent).length) {
|
|
462
|
+
result.columnContent = columnContent;
|
|
463
|
+
}
|
|
464
|
+
return result;
|
|
279
465
|
},
|
|
280
466
|
|
|
281
467
|
/**
|
|
@@ -323,11 +509,13 @@ module.exports = {
|
|
|
323
509
|
}
|
|
324
510
|
validateColumns(data.columns);
|
|
325
511
|
|
|
326
|
-
// Add columns
|
|
512
|
+
// Add columns, putting any preserved non-task content back where it was found
|
|
513
|
+
const columnContent = 'columnContent' in data && data.columnContent !== null ? data.columnContent : {};
|
|
514
|
+
validateColumnContent(columnContent);
|
|
327
515
|
for (let column in data.columns) {
|
|
328
516
|
result.push(
|
|
329
517
|
`## ${column}`,
|
|
330
|
-
data.columns[column]
|
|
518
|
+
buildColumnContent(data.columns[column], column in columnContent ? columnContent[column] : [])
|
|
331
519
|
);
|
|
332
520
|
}
|
|
333
521
|
} catch (error) {
|
package/src/utility.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const fuzzy = require('fuzzy');
|
|
2
|
+
|
|
1
3
|
module.exports = (() => {
|
|
2
4
|
const tags = {
|
|
3
5
|
b: 'bold',
|
|
@@ -38,6 +40,12 @@ module.exports = (() => {
|
|
|
38
40
|
return null;
|
|
39
41
|
}
|
|
40
42
|
const kanbn = await workspace.boardFromArgs(args);
|
|
43
|
+
|
|
44
|
+
// Actions are declarative rules with no code execution, so this is a way around a misbehaving
|
|
45
|
+
// rule rather than a safety control
|
|
46
|
+
if (args.actions === false || args['no-actions'] === true) {
|
|
47
|
+
kanbn.actionsEnabled = false;
|
|
48
|
+
}
|
|
41
49
|
if (!await kanbn.workspaceInitialised()) {
|
|
42
50
|
this.error('Kanbn has not been initialised in this folder\nTry running: {b}kanbn init{b}');
|
|
43
51
|
return null;
|
|
@@ -75,7 +83,7 @@ module.exports = (() => {
|
|
|
75
83
|
|
|
76
84
|
const targets = [];
|
|
77
85
|
for (let i = 0; i < slugs.length; i++) {
|
|
78
|
-
const kanbn = await this.resolveBoard(workspace, { board: slugs[i] });
|
|
86
|
+
const kanbn = await this.resolveBoard(workspace, { board: slugs[i], actions: args.actions });
|
|
79
87
|
if (kanbn === null) {
|
|
80
88
|
return null;
|
|
81
89
|
}
|
|
@@ -148,6 +156,76 @@ module.exports = (() => {
|
|
|
148
156
|
console.log(this.replaceTags(`\n{d}${boards.length} boards available -{d} {b}kanbn boards{b}`));
|
|
149
157
|
},
|
|
150
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Resolve a string to exactly one simple task on a board, for the commands that accept one.
|
|
161
|
+
* Real tasks always win, so this should only be called once no task has matched
|
|
162
|
+
* @param {object} kanbn The board-scoped Kanbn instance
|
|
163
|
+
* @param {string} input The title to match
|
|
164
|
+
* @param {Error|string} taskError The error to report if nothing matches, i.e. the error the
|
|
165
|
+
* command would have printed before simple tasks existed
|
|
166
|
+
* @param {?object[]} [matches=null] Matches already found, to save looking them up again
|
|
167
|
+
* @return {Promise<?object>} The matching simple task, or null if the error has been reported
|
|
168
|
+
*/
|
|
169
|
+
async resolveSimpleTask(kanbn, input, taskError, matches = null) {
|
|
170
|
+
if (matches === null) {
|
|
171
|
+
try {
|
|
172
|
+
matches = await kanbn.findSimpleTasks(input);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
this.error(error);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!matches.length) {
|
|
179
|
+
this.error(taskError);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Two lines with the same title are two different lines. Picking one of them silently would
|
|
184
|
+
// eventually move or delete the wrong one
|
|
185
|
+
if (matches.length > 1) {
|
|
186
|
+
this.error(
|
|
187
|
+
`"${input}" matches ${matches.length} simple tasks:\n` +
|
|
188
|
+
matches.map(match => ` "${match.text}" in column "${match.column}"`).join('\n') +
|
|
189
|
+
'\nEdit the board file directly, or rename one of them'
|
|
190
|
+
);
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
return matches[0];
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Report rules that were skipped during an operation
|
|
198
|
+
*
|
|
199
|
+
* A rule that quietly stops working is worse than no rule, so these go to stderr and aren't
|
|
200
|
+
* suppressed by verbose - the operation itself succeeded, which is why they aren't errors
|
|
201
|
+
* @param {object} kanbn The board-scoped Kanbn instance the operation ran on
|
|
202
|
+
*/
|
|
203
|
+
showActionWarnings(kanbn) {
|
|
204
|
+
const warnings = (kanbn && kanbn.lastActionWarnings) || [];
|
|
205
|
+
if (!warnings.length) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
for (const warning of warnings) {
|
|
209
|
+
console.error(this.replaceTags(`{d}action:{d} ${warning}`));
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Show a hint about tasks that a board references but which have no task file. These are skipped
|
|
215
|
+
* rather than fatal, so the hint is the only sign that the board is showing less than it lists
|
|
216
|
+
* @param {object[]} missing Missing task entries from kanbn.findMissingTaskFiles()
|
|
217
|
+
* @param {boolean} show False to suppress the hint
|
|
218
|
+
*/
|
|
219
|
+
showMissingTaskFilesHint(missing, show = true) {
|
|
220
|
+
if (!show || !missing.length) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const summary =
|
|
224
|
+
`${missing.length} ${missing.length === 1 ? 'task has' : 'tasks have'} no task file and ` +
|
|
225
|
+
`${missing.length === 1 ? 'was' : 'were'} skipped`;
|
|
226
|
+
console.log(this.replaceTags(`\n{d}${summary} - run{d} {b}kanbn validate{b} {d}for details{d}`));
|
|
227
|
+
},
|
|
228
|
+
|
|
151
229
|
/**
|
|
152
230
|
* Show a hint about tasks whose dates don't match the column they're in. Hints are suppressed
|
|
153
231
|
* for machine-readable output and when the board sets verbose to false
|
|
@@ -168,6 +246,62 @@ module.exports = (() => {
|
|
|
168
246
|
));
|
|
169
247
|
},
|
|
170
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Build the prompt definition for a task's assigned user
|
|
251
|
+
*
|
|
252
|
+
* With contributors configured this is an autocomplete over the list, which is where the typo
|
|
253
|
+
* mitigation comes from; without them it's the free-text input it has always been. Free text is
|
|
254
|
+
* accepted either way - contributors are advisory, never an enum, so a name that isn't in the
|
|
255
|
+
* list is still a perfectly good answer
|
|
256
|
+
* @param {object} options The prompt properties to merge in, i.e. name, message, default, when
|
|
257
|
+
* @param {object[]} contributors The workspace's normalised contributors
|
|
258
|
+
* @return {object} A prompt definition
|
|
259
|
+
*/
|
|
260
|
+
assignedPrompt(options, contributors) {
|
|
261
|
+
if (!contributors.length) {
|
|
262
|
+
return { type: 'input', ...options };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Suggestions are the canonical names, because that's the value written into the task file
|
|
266
|
+
const names = contributors.map(contributor => contributor.name);
|
|
267
|
+
return {
|
|
268
|
+
type: 'autocomplete',
|
|
269
|
+
suggestOnly: true,
|
|
270
|
+
source: (answers, input) => Promise.resolve(
|
|
271
|
+
fuzzy.filter(input || '', names).map(result => result.string)
|
|
272
|
+
),
|
|
273
|
+
...options
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Expand "@me" in a filter value to the current user
|
|
279
|
+
*
|
|
280
|
+
* The substitution is kept here, at the argument-parsing boundary: "@me" means something when
|
|
281
|
+
* filtering and nothing at all when setting a value, and a literal "@me" in a task file is
|
|
282
|
+
* nonsense nobody needs supported
|
|
283
|
+
* @param {object} kanbn The board-scoped Kanbn instance
|
|
284
|
+
* @param {string|string[]} value The filter value from the arguments
|
|
285
|
+
* @return {Promise<string|string[]|null>} The expanded value, or null if "@me" can't be resolved
|
|
286
|
+
*/
|
|
287
|
+
async expandCurrentUser(kanbn, value) {
|
|
288
|
+
const isMe = v => typeof v === 'string' && v.trim() === '@me';
|
|
289
|
+
const values = this.arrayArg(value);
|
|
290
|
+
if (!values.some(isMe)) {
|
|
291
|
+
return value;
|
|
292
|
+
}
|
|
293
|
+
const currentUser = await kanbn.currentUser();
|
|
294
|
+
if (!currentUser) {
|
|
295
|
+
this.error(
|
|
296
|
+
"Unable to work out who you are, so {b}@me{b} can't be resolved\n" +
|
|
297
|
+
'Set {b}KANBN_USER{b}, or set a git username with {b}git config user.name "your name"{b}'
|
|
298
|
+
);
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const expanded = values.map(v => (isMe(v) ? currentUser : v));
|
|
302
|
+
return Array.isArray(value) ? expanded : expanded[0];
|
|
303
|
+
},
|
|
304
|
+
|
|
171
305
|
/**
|
|
172
306
|
* Convert a string to simplified paramcase, e.g:
|
|
173
307
|
* PascalCase -> pascalcase
|