@basementuniverse/kanbn 1.1.0 → 2.0.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 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/coverage-summary.json +31 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +146 -0
- package/coverage/kanbn/index.html +116 -0
- package/coverage/kanbn/index.js.html +217 -0
- package/coverage/kanbn/src/board.js.html +646 -0
- package/coverage/kanbn/src/controller/add.js.html +1729 -0
- package/coverage/kanbn/src/controller/archive.js.html +277 -0
- package/coverage/kanbn/src/controller/board.js.html +217 -0
- package/coverage/kanbn/src/controller/burndown.js.html +607 -0
- package/coverage/kanbn/src/controller/comment.js.html +397 -0
- package/coverage/kanbn/src/controller/edit.js.html +2920 -0
- package/coverage/kanbn/src/controller/find.js.html +1597 -0
- package/coverage/kanbn/src/controller/gantt.js.html +1342 -0
- package/coverage/kanbn/src/controller/help.js.html +115 -0
- package/coverage/kanbn/src/controller/history.js.html +400 -0
- package/coverage/kanbn/src/controller/index.html +431 -0
- package/coverage/kanbn/src/controller/init.js.html +511 -0
- package/coverage/kanbn/src/controller/move.js.html +514 -0
- package/coverage/kanbn/src/controller/remove-all.js.html +226 -0
- package/coverage/kanbn/src/controller/remove.js.html +307 -0
- package/coverage/kanbn/src/controller/rename.js.html +397 -0
- package/coverage/kanbn/src/controller/restore.js.html +268 -0
- package/coverage/kanbn/src/controller/sort.js.html +1318 -0
- package/coverage/kanbn/src/controller/sprint.js.html +361 -0
- package/coverage/kanbn/src/controller/status.js.html +310 -0
- package/coverage/kanbn/src/controller/task.js.html +244 -0
- package/coverage/kanbn/src/controller/validate.js.html +367 -0
- package/coverage/kanbn/src/controller/version.js.html +100 -0
- package/coverage/kanbn/src/index.html +191 -0
- package/coverage/kanbn/src/main.js.html +9787 -0
- package/coverage/kanbn/src/parse-index.js.html +1063 -0
- package/coverage/kanbn/src/parse-markdown.js.html +202 -0
- package/coverage/kanbn/src/parse-task.js.html +2191 -0
- package/coverage/kanbn/src/utility.js.html +616 -0
- package/coverage/lcov.info +11837 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/tmp/coverage-214292-1787777191526-0.json +1 -0
- package/coverage/tmp/coverage-214293-1787777184569-0.json +1 -0
- package/docs/advanced-configuration.md +2 -0
- package/docs/commands/add.txt +10 -0
- package/docs/commands/board.txt +3 -1
- package/docs/commands/burndown.txt +2 -4
- package/docs/commands/edit.txt +22 -0
- package/docs/commands/find.txt +45 -1
- package/docs/commands/help.txt +1 -0
- package/docs/commands/move.txt +1 -1
- package/docs/commands/sort.txt +13 -2
- package/docs/commands/validate.txt +12 -0
- package/docs/filtering-and-sorting.md +218 -0
- package/docs/index-structure.md +68 -19
- package/docs/index.md +26 -2
- package/docs/migration-2.0.md +92 -0
- package/docs/quick-start.md +37 -1
- package/docs/task-structure.md +12 -3
- package/docs/views.md +229 -0
- package/example/README.md +70 -0
- package/example/advanced/.kanbn/index.md +26 -0
- package/example/advanced/.kanbn/tasks/add-rate-limiting.md +18 -0
- package/example/advanced/.kanbn/tasks/add-sso-support.md +15 -0
- package/example/advanced/.kanbn/tasks/migrate-to-new-cdn.md +14 -0
- package/example/advanced/.kanbn/tasks/refresh-marketing-site.md +14 -0
- package/example/advanced/.kanbn/tasks/rotate-api-keys.md +15 -0
- package/example/advanced/.kanbn/tasks/split-billing-service.md +17 -0
- package/example/advanced/.kanbn/tasks/upgrade-node-runtime.md +19 -0
- package/example/advanced/kanbn.yml +59 -0
- package/example/views/.kanbn/index.md +140 -0
- package/example/views/.kanbn/tasks/add-audit-log-export.md +18 -0
- package/example/views/.kanbn/tasks/add-dark-mode-toggle.md +16 -0
- package/example/views/.kanbn/tasks/build-webhook-retry-queue.md +15 -0
- package/example/views/.kanbn/tasks/fix-avatar-upload-crash.md +13 -0
- package/example/views/.kanbn/tasks/fix-expired-session-redirect.md +15 -0
- package/example/views/.kanbn/tasks/investigate-offline-mode.md +12 -0
- package/example/views/.kanbn/tasks/rewrite-search-indexer.md +12 -0
- package/example/views/.kanbn/tasks/tidy-up-settings-copy.md +12 -0
- package/package.json +4 -3
- package/routes/edit.json +2 -1
- package/routes/validate.json +4 -2
- package/skills/kanbn-plan/references/index-structure.md +1 -0
- package/skills/kanbn-plan/references/task-structure.md +2 -0
- package/src/board.js +1 -0
- package/src/controller/board.js +6 -0
- package/src/controller/edit.js +65 -12
- package/src/controller/gantt.js +46 -1
- package/src/controller/status.js +12 -0
- package/src/controller/task.js +1 -1
- package/src/controller/validate.js +78 -15
- package/src/main.js +324 -152
- package/src/parse-index.js +3 -0
- package/src/parse-markdown.js +3 -1
- package/src/parse-task.js +28 -1
- package/src/utility.js +26 -1
- /package/example/{.kanbn → basic/.kanbn}/index.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/add-basic-activity-feed.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/add-passwordless-login-option.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/add-usage-alert-email-thresholds.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/build-email-template-system.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/build-invoice-download-endpoint.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/build-tenant-settings-page.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/create-organization-switcher.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/create-sandbox-environment-provisioner.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/create-self-serve-cancellation-flow.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/define-product-pricing-strategy.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/design-onboarding-checklist.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/design-team-invite-expiry-flow.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/implement-data-retention-policy-jobs.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/implement-feature-flags-foundation.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/implement-project-creation-wizard.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/implement-stripe-webhook-signature-check.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/implement-team-permissions-ui.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/implement-user-signup-and-login.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/integrate-crm-lead-sync.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/legal-review-terms-and-privacy.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/migrate-legacy-events-to-new-schema.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/optimize-dashboard-first-load.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/prototype-report-export-scheduler.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/publish-internal-qa-checklist.md +0 -0
- /package/example/{.kanbn → basic/.kanbn}/tasks/setup-ci-pipeline.md +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
created: 2026-07-07T09:00:00.000Z
|
|
3
|
+
updated: 2026-07-14T09:00:00.000Z
|
|
4
|
+
started: 2026-07-11T09:00:00.000Z
|
|
5
|
+
assigned: Ben
|
|
6
|
+
progress: 0.3
|
|
7
|
+
due: 2026-10-01T17:00:00.000Z
|
|
8
|
+
tags:
|
|
9
|
+
- Feature
|
|
10
|
+
- Large
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Build Webhook Retry Queue
|
|
14
|
+
|
|
15
|
+
Retry failed webhook deliveries with exponential backoff.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
created: 2026-07-06T09:00:00.000Z
|
|
3
|
+
updated: 2026-07-12T09:00:00.000Z
|
|
4
|
+
started: 2026-07-10T09:00:00.000Z
|
|
5
|
+
assigned: Ana
|
|
6
|
+
progress: 0.5
|
|
7
|
+
due: 2026-07-25T17:00:00.000Z
|
|
8
|
+
tags:
|
|
9
|
+
- Bug
|
|
10
|
+
- Medium
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Fix Expired Session Redirect
|
|
14
|
+
|
|
15
|
+
Expired sessions redirect to the dashboard instead of the login page.
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basementuniverse/kanbn",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A CLI Kanban application",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "KANBN_ENV=test qunit ./test/**/*.test.js"
|
|
7
|
+
"test": "KANBN_ENV=test qunit ./test/**/*.test.js",
|
|
8
|
+
"coverage": "KANBN_ENV=test c8 --reporter=text --reporter=html --include=src/** --include=index.js qunit ./test/**/*.test.js"
|
|
8
9
|
},
|
|
9
10
|
"preferGlobal": true,
|
|
10
11
|
"bin": {
|
|
@@ -55,10 +56,10 @@
|
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@types/typescript": "^2.0.0",
|
|
59
|
+
"c8": "^12.0.0",
|
|
58
60
|
"capture-console": "^1.0.1",
|
|
59
61
|
"faker": "^5.1.0",
|
|
60
62
|
"mock-argv": "^1.1.10",
|
|
61
|
-
"mock-fs": "^4.13.0",
|
|
62
63
|
"mock-require": "^3.0.3",
|
|
63
64
|
"mock-stdin": "^1.0.0",
|
|
64
65
|
"mockdate": "^3.0.2",
|
package/routes/edit.json
CHANGED
package/routes/validate.json
CHANGED
|
@@ -38,6 +38,7 @@ Short project summary and planning assumptions.
|
|
|
38
38
|
- After the description, each level-2 heading defines a column.
|
|
39
39
|
- Each column should contain a markdown list of task links.
|
|
40
40
|
- Each task link should point to `tasks/<task-id>.md`.
|
|
41
|
+
- A task placed in a column listed in `startedColumns` or `completedColumns` should carry the matching `started` / `completed` date in its metadata. Kanbn reads task state from those dates, not from the column.
|
|
41
42
|
- The link text should be the task id, not the human-readable title.
|
|
42
43
|
|
|
43
44
|
## Recommended Defaults
|
|
@@ -106,6 +106,8 @@ Usually omit:
|
|
|
106
106
|
|
|
107
107
|
Add metadata only when the user supplied it or when the board already relies on it.
|
|
108
108
|
|
|
109
|
+
The one exception is a task you place in a started or completed column. Kanbn decides whether a task has started or been completed from its `started` and `completed` dates, not from the column it is in, so a task placed in `In Progress` or `Done` without those dates will be reported as drifted and will be missing from burndown charts. If you place a task in one of those columns because the user told you the work is already underway or finished, write the matching date. If you don't know when it happened, leave the task in a column that doesn't imply state.
|
|
110
|
+
|
|
109
111
|
Good uses of metadata:
|
|
110
112
|
|
|
111
113
|
- `tags` for epics, teams, domains, or sizing labels
|
package/src/board.js
CHANGED
|
@@ -35,6 +35,7 @@ module.exports = (() => {
|
|
|
35
35
|
started: 'started' in task.metadata ? formatDate(task.metadata.started, dateFormat) : '',
|
|
36
36
|
completed: 'completed' in task.metadata ? formatDate(task.metadata.completed, dateFormat) : '',
|
|
37
37
|
due: 'due' in task.metadata ? formatDate(task.metadata.due, dateFormat) : '',
|
|
38
|
+
assigned: 'assigned' in task.metadata ? task.metadata.assigned : '',
|
|
38
39
|
tags: 'tags' in task.metadata ? task.metadata.tags : [],
|
|
39
40
|
subTasks: task.subTasks,
|
|
40
41
|
relations: task.relations,
|
package/src/controller/board.js
CHANGED
|
@@ -32,6 +32,12 @@ module.exports = async args => {
|
|
|
32
32
|
// Show the board
|
|
33
33
|
board
|
|
34
34
|
.show(index, tasks, args.view, args.json)
|
|
35
|
+
.then(() => {
|
|
36
|
+
utility.showDateDriftHint(
|
|
37
|
+
kanbn.getDateDrift(index, tasks),
|
|
38
|
+
!args.json && kanbn.isVerbose(index)
|
|
39
|
+
);
|
|
40
|
+
})
|
|
35
41
|
.catch(error => {
|
|
36
42
|
utility.error(error);
|
|
37
43
|
});
|
package/src/controller/edit.js
CHANGED
|
@@ -412,7 +412,7 @@ async function interactive(taskData, taskIds, columnName, columnNames) {
|
|
|
412
412
|
type: 'input',
|
|
413
413
|
name: 'type',
|
|
414
414
|
message: 'Relation type:',
|
|
415
|
-
default: answers => taskData.relations.find(relation => relation.task === answers.selectRelation).
|
|
415
|
+
default: answers => taskData.relations.find(relation => relation.task === answers.selectRelation).type,
|
|
416
416
|
when: answers => answers.editRelation === 'edit'
|
|
417
417
|
}
|
|
418
418
|
]
|
|
@@ -420,15 +420,31 @@ async function interactive(taskData, taskIds, columnName, columnNames) {
|
|
|
420
420
|
]);
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Metadata fields that can be cleared with --unset, in addition to any custom fields defined in
|
|
425
|
+
* the index options
|
|
426
|
+
*/
|
|
427
|
+
const UNSETTABLE_FIELDS = [
|
|
428
|
+
'started',
|
|
429
|
+
'completed',
|
|
430
|
+
'due',
|
|
431
|
+
'plannedStart',
|
|
432
|
+
'plannedFinish',
|
|
433
|
+
'assigned',
|
|
434
|
+
'progress',
|
|
435
|
+
'tags'
|
|
436
|
+
];
|
|
437
|
+
|
|
423
438
|
/**
|
|
424
439
|
* Update a task
|
|
425
440
|
* @param {string} taskId
|
|
426
441
|
* @param {object} taskData
|
|
427
442
|
* @param {?string} columnName
|
|
443
|
+
* @param {string[]} [unsetFields=[]]
|
|
428
444
|
*/
|
|
429
|
-
function updateTask(taskId, taskData, columnName) {
|
|
445
|
+
function updateTask(taskId, taskData, columnName, unsetFields = []) {
|
|
430
446
|
kanbn
|
|
431
|
-
.updateTask(taskId, taskData, columnName)
|
|
447
|
+
.updateTask(taskId, taskData, columnName, unsetFields)
|
|
432
448
|
.then(taskId => {
|
|
433
449
|
console.log(`Updated task "${taskId}"`);
|
|
434
450
|
})
|
|
@@ -578,8 +594,8 @@ module.exports = async args => {
|
|
|
578
594
|
|
|
579
595
|
// Check that the sub-tasks being removed currently exist
|
|
580
596
|
for (let removedSubTask of removedSubTasks) {
|
|
581
|
-
if (taskData.subTasks.find(subTask => subTask.text === removedSubTask
|
|
582
|
-
utility.error(`Sub-task "${removedSubTask
|
|
597
|
+
if (taskData.subTasks.find(subTask => subTask.text === removedSubTask) === undefined) {
|
|
598
|
+
utility.error(`Sub-task "${removedSubTask}" doesn't exist`);
|
|
583
599
|
return;
|
|
584
600
|
}
|
|
585
601
|
}
|
|
@@ -633,7 +649,7 @@ module.exports = async args => {
|
|
|
633
649
|
// Check that the tags being removed currently exist
|
|
634
650
|
for (let removedTag of removedTags) {
|
|
635
651
|
if (taskData.metadata.tags.indexOf(removedTag) === -1) {
|
|
636
|
-
utility.error(`Tag "${
|
|
652
|
+
utility.error(`Tag "${removedTag}" doesn't exist`);
|
|
637
653
|
return;
|
|
638
654
|
}
|
|
639
655
|
}
|
|
@@ -652,8 +668,8 @@ module.exports = async args => {
|
|
|
652
668
|
|
|
653
669
|
// Check that the relations being removed currently exist
|
|
654
670
|
for (let removedRelation of removedRelations) {
|
|
655
|
-
if (taskData.relations.find(relation => relation.task === removedRelation
|
|
656
|
-
utility.error(`Relation "${removedRelation
|
|
671
|
+
if (taskData.relations.find(relation => relation.task === removedRelation) === undefined) {
|
|
672
|
+
utility.error(`Relation "${removedRelation}" doesn't exist`);
|
|
657
673
|
return;
|
|
658
674
|
}
|
|
659
675
|
}
|
|
@@ -754,6 +770,43 @@ module.exports = async args => {
|
|
|
754
770
|
}
|
|
755
771
|
}
|
|
756
772
|
|
|
773
|
+
// Unset metadata fields
|
|
774
|
+
const unsetFields = [];
|
|
775
|
+
if (args.unset) {
|
|
776
|
+
const customFieldNames = 'customFields' in index.options
|
|
777
|
+
? index.options.customFields.map(customField => customField.name)
|
|
778
|
+
: [];
|
|
779
|
+
const unsettableFields = [...UNSETTABLE_FIELDS, ...customFieldNames];
|
|
780
|
+
|
|
781
|
+
// Arguments that set the value of a metadata field, so that setting and unsetting the same
|
|
782
|
+
// field in one command can be rejected instead of silently resolving one way or the other
|
|
783
|
+
const settingArgs = {
|
|
784
|
+
due: args.due,
|
|
785
|
+
plannedStart: args.plannedStart,
|
|
786
|
+
plannedFinish: args.plannedFinish,
|
|
787
|
+
progress: args.progress,
|
|
788
|
+
assigned: args.assigned,
|
|
789
|
+
tags: args.tag,
|
|
790
|
+
...Object.fromEntries(customFieldNames.map(name => [name, args[name]]))
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
for (let field of utility.arrayArg(args.unset)) {
|
|
794
|
+
if (unsettableFields.indexOf(field) === -1) {
|
|
795
|
+
utility.error(
|
|
796
|
+
`Cannot unset "${field}"\nAvailable fields: ${unsettableFields.join(', ')}`
|
|
797
|
+
);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (settingArgs[field] !== undefined) {
|
|
801
|
+
utility.error(`Cannot set and unset "${field}" in the same command`);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (unsetFields.indexOf(field) === -1) {
|
|
805
|
+
unsetFields.push(field);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
757
810
|
// Update task interactively
|
|
758
811
|
if (args.interactive) {
|
|
759
812
|
interactive(taskData, taskIds, columnName, columnNames)
|
|
@@ -810,7 +863,7 @@ module.exports = async args => {
|
|
|
810
863
|
// Edit or remove sub-tasks
|
|
811
864
|
if ('editSubTasks' in answers) {
|
|
812
865
|
for (editSubTask of answers.editSubTasks) {
|
|
813
|
-
const i = taskData.subTasks.findIndex(subTask => subTask.
|
|
866
|
+
const i = taskData.subTasks.findIndex(subTask => subTask.text === editSubTask.selectSubTask);
|
|
814
867
|
if (i !== -1) {
|
|
815
868
|
switch (editSubTask.editSubTask) {
|
|
816
869
|
case 'remove':
|
|
@@ -837,7 +890,7 @@ module.exports = async args => {
|
|
|
837
890
|
// Remove tags
|
|
838
891
|
if ('removeTags' in answers && 'metadata' in taskData && 'tags' in taskData.metadata) {
|
|
839
892
|
for (removeTag of answers.removeTags) {
|
|
840
|
-
const i = taskData.metadata.tags.indexOf(removeTag.
|
|
893
|
+
const i = taskData.metadata.tags.indexOf(removeTag.selectTag);
|
|
841
894
|
if (i !== -1) {
|
|
842
895
|
taskData.metadata.tags.splice(i, 1);
|
|
843
896
|
}
|
|
@@ -878,7 +931,7 @@ module.exports = async args => {
|
|
|
878
931
|
|
|
879
932
|
// Update task
|
|
880
933
|
columnName = answers.column !== currentColumnName ? answers.column : null;
|
|
881
|
-
updateTask(taskId, taskData, columnName);
|
|
934
|
+
updateTask(taskId, taskData, columnName, unsetFields);
|
|
882
935
|
})
|
|
883
936
|
.catch(error => {
|
|
884
937
|
utility.error(error);
|
|
@@ -887,6 +940,6 @@ module.exports = async args => {
|
|
|
887
940
|
// Otherwise edit task non-interactively
|
|
888
941
|
} else {
|
|
889
942
|
columnName = columnName !== currentColumnName ? columnName : null;
|
|
890
|
-
updateTask(taskId, taskData, columnName);
|
|
943
|
+
updateTask(taskId, taskData, columnName, unsetFields);
|
|
891
944
|
}
|
|
892
945
|
};
|
package/src/controller/gantt.js
CHANGED
|
@@ -64,7 +64,8 @@ function createChartCell() {
|
|
|
64
64
|
return {
|
|
65
65
|
bg: null,
|
|
66
66
|
lineMask: 0,
|
|
67
|
-
arrowChar: null
|
|
67
|
+
arrowChar: null,
|
|
68
|
+
markerChar: null
|
|
68
69
|
};
|
|
69
70
|
}
|
|
70
71
|
|
|
@@ -121,6 +122,38 @@ function routeDependency(grid, sourceRow, sourceCol, targetRow, targetCol) {
|
|
|
121
122
|
grid[targetRow][targetCol].arrowChar = targetCol >= routeCol ? '→' : '←';
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
function setMarker(cell, markerChar) {
|
|
126
|
+
if (!cell.arrowChar && !cell.markerChar) {
|
|
127
|
+
cell.markerChar = markerChar;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function routeIncomingExternalDependency(grid, row, targetCol) {
|
|
132
|
+
const arrowCol = Math.max(0, targetCol);
|
|
133
|
+
grid[row][arrowCol].arrowChar = '→';
|
|
134
|
+
|
|
135
|
+
if (arrowCol > 0) {
|
|
136
|
+
addMask(grid[row][arrowCol], DIR_LEFT);
|
|
137
|
+
addMask(grid[row][arrowCol - 1], DIR_RIGHT);
|
|
138
|
+
}
|
|
139
|
+
if (arrowCol > 1) {
|
|
140
|
+
setMarker(grid[row][arrowCol - 2], '…');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function routeOutgoingExternalDependency(grid, row, sourceCol, width) {
|
|
145
|
+
const arrowCol = Math.min(width - 1, sourceCol);
|
|
146
|
+
grid[row][arrowCol].arrowChar = '→';
|
|
147
|
+
|
|
148
|
+
if (arrowCol < width - 1) {
|
|
149
|
+
addMask(grid[row][arrowCol], DIR_RIGHT);
|
|
150
|
+
addMask(grid[row][arrowCol + 1], DIR_LEFT);
|
|
151
|
+
}
|
|
152
|
+
if (arrowCol < width - 2) {
|
|
153
|
+
setMarker(grid[row][arrowCol + 2], '…');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
124
157
|
function buildDependencyOverlay(tasks, barGeometries, width) {
|
|
125
158
|
const grid = createChartGrid(tasks.length, width);
|
|
126
159
|
const taskRowMap = new Map(tasks.map((task, index) => [task.id, index]));
|
|
@@ -140,6 +173,14 @@ function buildDependencyOverlay(tasks, barGeometries, width) {
|
|
|
140
173
|
barGeometries[targetRow].start
|
|
141
174
|
);
|
|
142
175
|
});
|
|
176
|
+
|
|
177
|
+
if (Array.isArray(task.externalIncomingDependencies) && task.externalIncomingDependencies.length > 0) {
|
|
178
|
+
routeIncomingExternalDependency(grid, targetRow, barGeometries[targetRow].start);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (Array.isArray(task.externalOutgoingDependents) && task.externalOutgoingDependents.length > 0) {
|
|
182
|
+
routeOutgoingExternalDependency(grid, targetRow, barGeometries[targetRow].end, width);
|
|
183
|
+
}
|
|
143
184
|
});
|
|
144
185
|
|
|
145
186
|
return grid;
|
|
@@ -165,6 +206,10 @@ function renderChartRow(gridRow, barGeometry, barBackground, nowPosition) {
|
|
|
165
206
|
return styleCell(lineChar, ANSI_FG_LINE, bg);
|
|
166
207
|
}
|
|
167
208
|
|
|
209
|
+
if (cell.markerChar) {
|
|
210
|
+
return styleCell(cell.markerChar, ANSI_FG_LINE, bg);
|
|
211
|
+
}
|
|
212
|
+
|
|
168
213
|
if (nowPosition === column) {
|
|
169
214
|
return styleCell(NOW_MARKER, ANSI_FG_NOW, bg);
|
|
170
215
|
}
|
package/src/controller/status.js
CHANGED
|
@@ -57,6 +57,18 @@ module.exports = async args => {
|
|
|
57
57
|
console.log(args.json ? JSON.stringify(output, null, 2) : yaml.stringify(output, 4, 2));
|
|
58
58
|
}
|
|
59
59
|
})
|
|
60
|
+
.then(async () => {
|
|
61
|
+
|
|
62
|
+
// Only load tasks for the hint when it would actually be shown
|
|
63
|
+
if (args.json || args.quiet) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const index = await kanbn.getIndex();
|
|
67
|
+
if (!kanbn.isVerbose(index)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
utility.showDateDriftHint(await kanbn.findDateDrift());
|
|
71
|
+
})
|
|
60
72
|
.catch(error => {
|
|
61
73
|
utility.error(error);
|
|
62
74
|
});
|
package/src/controller/task.js
CHANGED
|
@@ -2,6 +2,31 @@ const kanbn = require('../main');
|
|
|
2
2
|
const utility = require('../utility');
|
|
3
3
|
const yaml = require('yamljs');
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Show a list of tasks whose dates disagree with the column they're in
|
|
7
|
+
* @param {object[]} drift
|
|
8
|
+
* @param {boolean} json
|
|
9
|
+
*/
|
|
10
|
+
function showDrift(drift, json) {
|
|
11
|
+
if (json) {
|
|
12
|
+
console.log(JSON.stringify(drift, null, 2));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const fixable = drift.filter(d => d.fixable);
|
|
16
|
+
console.log(
|
|
17
|
+
`${drift.length} ${drift.length === 1 ? 'task has' : 'tasks have'} dates that don't match ` +
|
|
18
|
+
`${drift.length === 1 ? 'its' : 'their'} column:`
|
|
19
|
+
);
|
|
20
|
+
for (const item of drift) {
|
|
21
|
+
console.log(` ${item.task}: ${item.message}`);
|
|
22
|
+
}
|
|
23
|
+
if (fixable.length) {
|
|
24
|
+
console.log(utility.replaceTags(
|
|
25
|
+
`\nRun {b}kanbn validate --fix{b} to fill in ${fixable.length === 1 ? 'the missing date' : 'the missing dates'}.`
|
|
26
|
+
));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
5
30
|
module.exports = async args => {
|
|
6
31
|
|
|
7
32
|
// Make sure kanbn has been initialised
|
|
@@ -11,21 +36,59 @@ module.exports = async args => {
|
|
|
11
36
|
}
|
|
12
37
|
|
|
13
38
|
// Validate kanbn files
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
39
|
+
let result;
|
|
40
|
+
try {
|
|
41
|
+
result = await kanbn.validate(args.save);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
utility.error(error);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (result !== true) {
|
|
47
|
+
utility.error(
|
|
48
|
+
`${result.length} errors found in task files:\n${(
|
|
49
|
+
args.json
|
|
50
|
+
? JSON.stringify(result, null, 2)
|
|
51
|
+
: yaml.stringify(result, 4, 2)
|
|
52
|
+
)}`
|
|
53
|
+
);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Backfill missing started and completed dates
|
|
58
|
+
if (args.fix) {
|
|
59
|
+
let fixed;
|
|
60
|
+
try {
|
|
61
|
+
fixed = await kanbn.fixDateDrift();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
utility.error(error);
|
|
64
|
+
return;
|
|
26
65
|
}
|
|
27
|
-
|
|
28
|
-
|
|
66
|
+
if (!fixed.length) {
|
|
67
|
+
console.log('Everything OK, nothing to fix');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (args.json) {
|
|
71
|
+
console.log(JSON.stringify(fixed, null, 2));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
console.log(`Filled in ${fixed.length} missing ${fixed.length === 1 ? 'date' : 'dates'}:`);
|
|
75
|
+
for (const item of fixed) {
|
|
76
|
+
console.log(` ${item.task}: ${item.field} set to ${item.date.toISOString()} (from ${item.source})`);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Report tasks whose dates disagree with their column
|
|
82
|
+
let drift;
|
|
83
|
+
try {
|
|
84
|
+
drift = await kanbn.findDateDrift();
|
|
85
|
+
} catch (error) {
|
|
29
86
|
utility.error(error);
|
|
30
|
-
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (drift.length) {
|
|
90
|
+
showDrift(drift, args.json);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
console.log('Everything OK');
|
|
31
94
|
};
|