@harborppm/cli 0.86.0 → 0.87.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/dist/app.mjs +138 -23
- package/lib/cli.mjs +30 -4
- package/package.json +1 -1
package/dist/app.mjs
CHANGED
|
@@ -1898,7 +1898,20 @@ function boardPositions(tasks) {
|
|
|
1898
1898
|
return out;
|
|
1899
1899
|
});
|
|
1900
1900
|
}
|
|
1901
|
-
|
|
1901
|
+
/** For each task id, the tasks that name it as their parent, in list order (src/lib/subtasks.ts reads it). */
|
|
1902
|
+
function childrenByParent(tasks) {
|
|
1903
|
+
return cached$1(children, tasks, () => {
|
|
1904
|
+
const out = /* @__PURE__ */ new Map();
|
|
1905
|
+
for (const t of tasks) {
|
|
1906
|
+
if (!t.parentTaskId) continue;
|
|
1907
|
+
const list = out.get(t.parentTaskId);
|
|
1908
|
+
if (list) list.push(t);
|
|
1909
|
+
else out.set(t.parentTaskId, [t]);
|
|
1910
|
+
}
|
|
1911
|
+
return out;
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
var ids$1, projectTasks$1, EMPTY, waiting, positions, children;
|
|
1902
1915
|
var init_indexes = __esmMin((() => {
|
|
1903
1916
|
init_blockers();
|
|
1904
1917
|
ids$1 = /* @__PURE__ */ new WeakMap();
|
|
@@ -1906,6 +1919,7 @@ var init_indexes = __esmMin((() => {
|
|
|
1906
1919
|
EMPTY = Object.freeze([]);
|
|
1907
1920
|
waiting = /* @__PURE__ */ new WeakMap();
|
|
1908
1921
|
positions = /* @__PURE__ */ new WeakMap();
|
|
1922
|
+
children = /* @__PURE__ */ new WeakMap();
|
|
1909
1923
|
}));
|
|
1910
1924
|
|
|
1911
1925
|
// src/lib/premortem.ts
|
|
@@ -4765,6 +4779,54 @@ var init_task_people = __esmMin((() => {
|
|
|
4765
4779
|
init_agents();
|
|
4766
4780
|
}));
|
|
4767
4781
|
|
|
4782
|
+
// src/lib/subtasks.ts
|
|
4783
|
+
/** The parent `task` is a piece of, when that link holds; otherwise null. */
|
|
4784
|
+
function parentOf(task, tasks) {
|
|
4785
|
+
const id = task.parentTaskId;
|
|
4786
|
+
if (!id || id === task.id) return null;
|
|
4787
|
+
const parent = byId(tasks).get(id);
|
|
4788
|
+
if (!parent || parent.projectId !== task.projectId || parent.parentTaskId) return null;
|
|
4789
|
+
return parent;
|
|
4790
|
+
}
|
|
4791
|
+
/** Whether `task` shows inside its parent rather than on its own. */
|
|
4792
|
+
function isSubtask(task, tasks) {
|
|
4793
|
+
return parentOf(task, tasks) !== null;
|
|
4794
|
+
}
|
|
4795
|
+
/** The subtasks of `parent`, in list order. Empty for a task that is itself a subtask. */
|
|
4796
|
+
function subtasksOf(parent, tasks) {
|
|
4797
|
+
if (isSubtask(parent, tasks)) return NONE$2;
|
|
4798
|
+
const list = childrenByParent(tasks).get(parent.id);
|
|
4799
|
+
if (!list) return NONE$2;
|
|
4800
|
+
return list.filter((t) => t.projectId === parent.projectId && t.id !== parent.id);
|
|
4801
|
+
}
|
|
4802
|
+
/** The tasks that stand on their own: what a board or a list shows. */
|
|
4803
|
+
function topLevel(list, tasks) {
|
|
4804
|
+
return list.filter((t) => !isSubtask(t, tasks));
|
|
4805
|
+
}
|
|
4806
|
+
/**
|
|
4807
|
+
* Why `childId` can't become a subtask of `parentId`, or null when it can.
|
|
4808
|
+
* The parent must be another task in the same project that isn't a subtask,
|
|
4809
|
+
* and the child mustn't have subtasks of its own (one level only).
|
|
4810
|
+
*/
|
|
4811
|
+
function subtaskProblem(childId, parentId, tasks) {
|
|
4812
|
+
const index = byId(tasks);
|
|
4813
|
+
const child = index.get(childId);
|
|
4814
|
+
const parent = index.get(parentId);
|
|
4815
|
+
if (!parent) return "That task isn't here any more.";
|
|
4816
|
+
if (childId === parentId) return "A task can't be a subtask of itself.";
|
|
4817
|
+
if (child && child.projectId !== parent.projectId) return "A subtask has to be in the same project as its parent.";
|
|
4818
|
+
if (isSubtask(parent, tasks)) return "That task is already a subtask; subtasks go one level deep.";
|
|
4819
|
+
if (child && subtasksOf(child, tasks).length > 0) return "This task has subtasks of its own, so it can't go inside another.";
|
|
4820
|
+
if (subtasksOf(parent, tasks).filter((t) => t.id !== childId).length >= 50) return `A task can hold up to ${50} subtasks.`;
|
|
4821
|
+
return null;
|
|
4822
|
+
}
|
|
4823
|
+
var SUBTASKS_MAX, NONE$2;
|
|
4824
|
+
var init_subtasks = __esmMin((() => {
|
|
4825
|
+
init_indexes();
|
|
4826
|
+
SUBTASKS_MAX = 50;
|
|
4827
|
+
NONE$2 = Object.freeze([]);
|
|
4828
|
+
}));
|
|
4829
|
+
|
|
4768
4830
|
// src/lib/board.ts
|
|
4769
4831
|
/** The column's cap after "No cap" (`uncapped`) or a number is picked. */
|
|
4770
4832
|
function withWipLimit(col, limit) {
|
|
@@ -4943,11 +5005,12 @@ function wipFullMessage(overflow) {
|
|
|
4943
5005
|
function columnWip(data, projectId, status, extra = 1) {
|
|
4944
5006
|
const col = boardConfigOf(data, projectId).columns.find((c) => c.status === status);
|
|
4945
5007
|
if (!col) return null;
|
|
4946
|
-
const current = tasksOf(data.tasks, projectId).filter((t) => t.status === status).length;
|
|
5008
|
+
const current = tasksOf(data.tasks, projectId).filter((t) => t.status === status && !isSubtask(t, data.tasks)).length;
|
|
4947
5009
|
return wipOverflow(col, current, extra);
|
|
4948
5010
|
}
|
|
4949
5011
|
var BOARD_PRESETS, ALL_FIELDS, DEFAULT_DOING_WIP, DEFAULT_REVIEW_WIP, WIP_CAP_CHOICES;
|
|
4950
5012
|
var init_board = __esmMin((() => {
|
|
5013
|
+
init_subtasks();
|
|
4951
5014
|
init_indexes();
|
|
4952
5015
|
init_swatches();
|
|
4953
5016
|
BOARD_PRESETS = [
|
|
@@ -5979,6 +6042,7 @@ init_date_fns();
|
|
|
5979
6042
|
init_date_text();
|
|
5980
6043
|
init_first_voyage();
|
|
5981
6044
|
init_indexes();
|
|
6045
|
+
init_subtasks();
|
|
5982
6046
|
init_tide();
|
|
5983
6047
|
init_format();
|
|
5984
6048
|
init_raid();
|
|
@@ -6049,7 +6113,7 @@ function myRaid(data, personId) {
|
|
|
6049
6113
|
/** A project's board: its visible columns, in order, with their tasks. */
|
|
6050
6114
|
function boardOf(data, projectId) {
|
|
6051
6115
|
const config = boardConfigOf(data, projectId);
|
|
6052
|
-
const tasks = tasksOf(data.tasks, projectId);
|
|
6116
|
+
const tasks = topLevel(tasksOf(data.tasks, projectId), data.tasks);
|
|
6053
6117
|
return visibleColumns(config).map((col) => ({
|
|
6054
6118
|
status: col.status,
|
|
6055
6119
|
label: columnLabel(col),
|
|
@@ -6065,7 +6129,7 @@ function neighbourStatus(data, projectId, status, step) {
|
|
|
6065
6129
|
return cols[at + step] ?? null;
|
|
6066
6130
|
}
|
|
6067
6131
|
function projectTasks(data, projectId) {
|
|
6068
|
-
return
|
|
6132
|
+
return topLevel(tasksOf(data.tasks, projectId), data.tasks).sort((a, b) => STATUS_ORDER.indexOf(a.status) - STATUS_ORDER.indexOf(b.status) || (a.number ?? 0) - (b.number ?? 0));
|
|
6069
6133
|
}
|
|
6070
6134
|
/**
|
|
6071
6135
|
* The project a new task goes in when none was picked: the only open one,
|
|
@@ -19822,11 +19886,7 @@ function fieldValueText(def, value, nameOf) {
|
|
|
19822
19886
|
}
|
|
19823
19887
|
|
|
19824
19888
|
// src/lib/exports.ts
|
|
19825
|
-
|
|
19826
|
-
* The spreadsheets Harbor hands out: a project's tasks, the risk register and
|
|
19827
|
-
* the review. The website downloads them, agents and the terminal get the
|
|
19828
|
-
* same text (the export_csv tool, `harbor export`).
|
|
19829
|
-
*/
|
|
19889
|
+
init_subtasks();
|
|
19830
19890
|
init_derived_health();
|
|
19831
19891
|
init_priority();
|
|
19832
19892
|
init_raid();
|
|
@@ -19861,6 +19921,7 @@ function tasksCsv(data, projectId) {
|
|
|
19861
19921
|
"Done",
|
|
19862
19922
|
"Blocked by",
|
|
19863
19923
|
"Description",
|
|
19924
|
+
"Subtask of",
|
|
19864
19925
|
...fields.map((f) => f.name)
|
|
19865
19926
|
], tasks.map((t) => [
|
|
19866
19927
|
taskKey(t, project) ?? "",
|
|
@@ -19872,6 +19933,7 @@ function tasksCsv(data, projectId) {
|
|
|
19872
19933
|
t.status === "done" ? "yes" : "no",
|
|
19873
19934
|
blockerIds(t).map((id) => here.get(id)).filter((b) => Boolean(b)).map(name).join("; "),
|
|
19874
19935
|
t.description,
|
|
19936
|
+
((p) => p ? name(p) : "")(parentOf(t, data.tasks)),
|
|
19875
19937
|
...fields.map((f) => f.type === "url" ? safeLink(t.customFieldValues?.[f.id]) ?? "" : fieldValueText(f, t.customFieldValues?.[f.id], nameOf))
|
|
19876
19938
|
]));
|
|
19877
19939
|
}
|
|
@@ -20482,16 +20544,7 @@ function approvalsWaitingOn(tasks, personId) {
|
|
|
20482
20544
|
}
|
|
20483
20545
|
|
|
20484
20546
|
// src/lib/mcp/tools.ts
|
|
20485
|
-
|
|
20486
|
-
* What an agent can read about a harbor, as MCP tools. Each tool is a pure
|
|
20487
|
-
* function of one harbor's document (health already stamped) and who is
|
|
20488
|
-
* asking, so it is tested against the demo without a server. The route in
|
|
20489
|
-
* src/routes/api/mcp.ts signs the caller in, picks the harbor and loads it.
|
|
20490
|
-
*
|
|
20491
|
-
* Read only: nothing here changes a workspace. Answers carry ids so an agent
|
|
20492
|
-
* can ask about one thing next, and every list is capped and pageable, since
|
|
20493
|
-
* a harbor can hold thousands of tasks.
|
|
20494
|
-
*/
|
|
20547
|
+
init_subtasks();
|
|
20495
20548
|
init_thanks();
|
|
20496
20549
|
init_task_keys();
|
|
20497
20550
|
init_brief();
|
|
@@ -21267,7 +21320,7 @@ const HARBOR_TOOLS = [
|
|
|
21267
21320
|
tool({
|
|
21268
21321
|
name: "get_task",
|
|
21269
21322
|
title: "Task details",
|
|
21270
|
-
description: "One task: its owner (assignee) and anyone also on it (alsoOn), notes, checklist, its files, what it waits on, what waits on it, its latest comments, the earliest due date its owner's queue suggests (pilotDue: fits says whether the due date holds; absent with no owner, an agent owner, or once done), its place in its board column (`position`, from 1 at the top), time logged on it (timeLogged: the total and the latest entries, when there is any), its values for the project's own fields by name (fields, when it has any; set them with set_field), and its url on the website." + DATA_NOTE,
|
|
21323
|
+
description: "One task: its owner (assignee) and anyone also on it (alsoOn), notes, checklist, its files, what it waits on, what waits on it, its latest comments, the earliest due date its owner's queue suggests (pilotDue: fits says whether the due date holds; absent with no owner, an agent owner, or once done), its place in its board column (`position`, from 1 at the top), time logged on it (timeLogged: the total and the latest entries, when there is any), its values for the project's own fields by name (fields, when it has any; set them with set_field), its subtasks or the task it's a subtask of (subtaskOf), and its url on the website." + DATA_NOTE,
|
|
21271
21324
|
input: object({
|
|
21272
21325
|
harbor: harborArg,
|
|
21273
21326
|
task: string().min(1).max(100).describe("Task id or key (\"RR-42\"), from search_tasks or my_work.")
|
|
@@ -21319,6 +21372,12 @@ const HARBOR_TOOLS = [
|
|
|
21319
21372
|
...t.time?.length ? { timeLogged: timeOut(t, people) } : {},
|
|
21320
21373
|
waitingOn: blockersOf(t, data.tasks).map(brief),
|
|
21321
21374
|
waitedOnBy: waiters.map(brief),
|
|
21375
|
+
...((parent) => parent ? { subtaskOf: brief(parent) } : {})(parentOf(t, data.tasks)),
|
|
21376
|
+
...((subs) => subs.length ? { subtasks: subs.map((x) => ({
|
|
21377
|
+
...brief(x),
|
|
21378
|
+
assignee: nameOf$2(people, x.assigneeId),
|
|
21379
|
+
due: dayOf(x.dueDate)
|
|
21380
|
+
})) } : {})(subtasksOf(t, data.tasks)),
|
|
21322
21381
|
...t.sounding ? { sounding: soundingOut(data, t, me(ctx)?.id ?? null, ctx.now) } : {},
|
|
21323
21382
|
...t.approval ? { approval: approvalOut(data, t.approval) } : {},
|
|
21324
21383
|
...((v) => v ? { fields: v } : {})(fieldValuesOut(data, project, t)),
|
|
@@ -23888,7 +23947,9 @@ function tasksSlice({ set, get, write }) {
|
|
|
23888
23947
|
get().log(`added ${who?.name ?? "someone"} as a second pair of hands on ${taskIds.length + raidIds.length} item${taskIds.length + raidIds.length === 1 ? "" : "s"}`, "/people");
|
|
23889
23948
|
},
|
|
23890
23949
|
addTask: (t, opts) => {
|
|
23891
|
-
|
|
23950
|
+
const wanted = t.parentTaskId ? byId(get().tasks).get(t.parentTaskId) : void 0;
|
|
23951
|
+
const parentTaskId = wanted && wanted.projectId === t.projectId && !subtaskProblem("", wanted.id, get().tasks) ? wanted.id : null;
|
|
23952
|
+
if (!opts?.allowOverCap && !parentTaskId) {
|
|
23892
23953
|
if (columnWip(get(), t.projectId, t.status, 1)) return "";
|
|
23893
23954
|
}
|
|
23894
23955
|
const id = uid("t");
|
|
@@ -23899,6 +23960,7 @@ function tasksSlice({ set, get, write }) {
|
|
|
23899
23960
|
createdById: get().currentUserId || null,
|
|
23900
23961
|
completedAt: t.status === "done" ? t.completedAt ?? stamp() : null,
|
|
23901
23962
|
checklist: t.checklist ?? [],
|
|
23963
|
+
..."parentTaskId" in t ? { parentTaskId } : {},
|
|
23902
23964
|
...blockersPatch(blockerIds({
|
|
23903
23965
|
id,
|
|
23904
23966
|
blockedById: t.blockedById,
|
|
@@ -23925,6 +23987,10 @@ function tasksSlice({ set, get, write }) {
|
|
|
23925
23987
|
},
|
|
23926
23988
|
updateTask: (id, patch, opts) => {
|
|
23927
23989
|
const task = get().tasks.find((row) => row.id === id);
|
|
23990
|
+
if (task && patch.parentTaskId && subtaskProblem(id, patch.parentTaskId, get().tasks)) {
|
|
23991
|
+
patch = { ...patch };
|
|
23992
|
+
delete patch.parentTaskId;
|
|
23993
|
+
}
|
|
23928
23994
|
if (task && patch.status && patch.status !== task.status) {
|
|
23929
23995
|
if (patch.status === "done" && !opts?.allowBlocked && isBlocked$1(task, get().tasks)) {
|
|
23930
23996
|
set({ blockPending: {
|
|
@@ -23936,13 +24002,24 @@ function tasksSlice({ set, get, write }) {
|
|
|
23936
24002
|
} });
|
|
23937
24003
|
return;
|
|
23938
24004
|
}
|
|
23939
|
-
if (!opts?.allowOverCap) {
|
|
24005
|
+
if (!opts?.allowOverCap && !isSubtask(task, get().tasks)) {
|
|
23940
24006
|
if (columnWip(get(), task.projectId, patch.status, 1)) return;
|
|
23941
24007
|
}
|
|
23942
24008
|
}
|
|
24009
|
+
const moving = task && patch.projectId && patch.projectId !== task.projectId;
|
|
24010
|
+
const carried = moving ? new Set(subtasksOf(task, get().tasks).map((t) => t.id)) : null;
|
|
24011
|
+
if (moving && task.parentTaskId && !("parentTaskId" in patch)) patch = {
|
|
24012
|
+
...patch,
|
|
24013
|
+
parentTaskId: null
|
|
24014
|
+
};
|
|
23943
24015
|
const project = get().projects.find((p) => p.id === task?.projectId);
|
|
23944
24016
|
const newComments = [];
|
|
23945
24017
|
const tasks = get().tasks.map((row) => {
|
|
24018
|
+
if (carried?.has(row.id)) return {
|
|
24019
|
+
...row,
|
|
24020
|
+
projectId: patch.projectId,
|
|
24021
|
+
touchedAt: stamp()
|
|
24022
|
+
};
|
|
23946
24023
|
if (row.id !== id) return row;
|
|
23947
24024
|
const next = {
|
|
23948
24025
|
...row,
|
|
@@ -23992,7 +24069,7 @@ function tasksSlice({ set, get, write }) {
|
|
|
23992
24069
|
} });
|
|
23993
24070
|
return;
|
|
23994
24071
|
}
|
|
23995
|
-
if (!opts?.allowOverCap) {
|
|
24072
|
+
if (!opts?.allowOverCap && !isSubtask(task, get().tasks)) {
|
|
23996
24073
|
if (columnWip(get(), task.projectId, status, 1)) return;
|
|
23997
24074
|
}
|
|
23998
24075
|
const nextRow = withStatus(task, status);
|
|
@@ -24412,6 +24489,7 @@ var init_tasks = __esmMin((() => {
|
|
|
24412
24489
|
init_assignment();
|
|
24413
24490
|
init_bulk();
|
|
24414
24491
|
init_task_people();
|
|
24492
|
+
init_subtasks();
|
|
24415
24493
|
}));
|
|
24416
24494
|
|
|
24417
24495
|
// src/lib/raid-history.ts
|
|
@@ -30471,6 +30549,7 @@ init_clock();
|
|
|
30471
30549
|
init_date_text();
|
|
30472
30550
|
init_work();
|
|
30473
30551
|
init_blockers();
|
|
30552
|
+
init_subtasks();
|
|
30474
30553
|
init_recurrence();
|
|
30475
30554
|
init_task_people();
|
|
30476
30555
|
init_search();
|
|
@@ -30589,6 +30668,8 @@ function TaskPanel({ taskId, width, height, go, active = true, onClose }) {
|
|
|
30589
30668
|
const thread = comments.filter((c) => c.taskId === task.id).slice(-4);
|
|
30590
30669
|
const mine = comments.filter((c) => c.taskId === task.id && c.authorId === me);
|
|
30591
30670
|
const blockers = blockersOf(task, tasks);
|
|
30671
|
+
const subtaskOf = parentOf(task, tasks);
|
|
30672
|
+
const subs = subtasksOf(task, tasks);
|
|
30592
30673
|
const done = () => setEditing(null);
|
|
30593
30674
|
const nameOf = (id) => people.find((p) => p.id === id)?.name;
|
|
30594
30675
|
const setCustom = (fieldId, value) => store.getState().setTaskCustomFieldValue(task.id, fieldId, value);
|
|
@@ -30941,6 +31022,40 @@ function TaskPanel({ taskId, width, height, go, active = true, onClose }) {
|
|
|
30941
31022
|
]
|
|
30942
31023
|
}, item.id);
|
|
30943
31024
|
}),
|
|
31025
|
+
subtaskOf ? /* @__PURE__ */ jsxs(Text, {
|
|
31026
|
+
color: theme.muted,
|
|
31027
|
+
wrap: "truncate-end",
|
|
31028
|
+
children: [
|
|
31029
|
+
" ",
|
|
31030
|
+
"Subtask of ",
|
|
31031
|
+
keyOf$1({ projects }, subtaskOf),
|
|
31032
|
+
" ",
|
|
31033
|
+
subtaskOf.title
|
|
31034
|
+
]
|
|
31035
|
+
}) : null,
|
|
31036
|
+
subs.length > 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
31037
|
+
color: theme.muted,
|
|
31038
|
+
children: [
|
|
31039
|
+
" ",
|
|
31040
|
+
"Subtasks ",
|
|
31041
|
+
subs.filter((x) => x.status === "done").length,
|
|
31042
|
+
"/",
|
|
31043
|
+
subs.length
|
|
31044
|
+
]
|
|
31045
|
+
}) : null,
|
|
31046
|
+
subs.map((x) => /* @__PURE__ */ jsxs(Text, {
|
|
31047
|
+
wrap: "truncate-end",
|
|
31048
|
+
children: [
|
|
31049
|
+
" ",
|
|
31050
|
+
x.status === "done" ? "[x] " : "[ ] ",
|
|
31051
|
+
keyOf$1({ projects }, x),
|
|
31052
|
+
" ",
|
|
31053
|
+
x.status === "done" ? /* @__PURE__ */ jsx(Text, {
|
|
31054
|
+
strikethrough: true,
|
|
31055
|
+
children: x.title
|
|
31056
|
+
}) : x.title
|
|
31057
|
+
]
|
|
31058
|
+
}, x.id)),
|
|
30944
31059
|
(task.github ?? []).slice(0, 3).map((ref) => /* @__PURE__ */ jsxs(Text, {
|
|
30945
31060
|
color: theme.muted,
|
|
30946
31061
|
wrap: "truncate-end",
|
package/lib/cli.mjs
CHANGED
|
@@ -168,7 +168,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
168
168
|
[--also "NAME, NAME"] [--due DATE|pilot]
|
|
169
169
|
[--priority low|medium|high|urgent]
|
|
170
170
|
[--status S] [--comment TEXT] [--over-limit] [--dry-run]
|
|
171
|
-
[--literal] [--template NAME]
|
|
171
|
+
[--literal] [--template NAME] [--parent TASK]
|
|
172
172
|
reads plain words in TITLE as the
|
|
173
173
|
website does: @name, by friday,
|
|
174
174
|
!high, #project (--literal: keep
|
|
@@ -183,7 +183,8 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
183
183
|
[--also "NAME, NAME"|none] [--due DATE|pilot|none]
|
|
184
184
|
[--priority P] [--status S] [--notes TEXT]
|
|
185
185
|
[--repeat weekly|biweekly|monthly|never]
|
|
186
|
-
[--waits-on TASK,TASK|none] [--
|
|
186
|
+
[--waits-on TASK,TASK|none] [--parent TASK|none]
|
|
187
|
+
[--confirm] [--over-limit]
|
|
187
188
|
harbor limit PROJECT [COLUMN N|none] how many tasks a board column
|
|
188
189
|
takes at once; the owner sets it
|
|
189
190
|
harbor week [--next|--last|--week DATE] [--review]
|
|
@@ -704,7 +705,7 @@ export function hint(message, project) {
|
|
|
704
705
|
)
|
|
705
706
|
.replace(
|
|
706
707
|
/^Nothing to change: pass a field to update\.$/,
|
|
707
|
-
"Nothing to change. Say what with --title, --assignee, --also, --due, --priority, --status, --notes, --repeat
|
|
708
|
+
"Nothing to change. Say what with --title, --assignee, --also, --due, --priority, --status, --notes, --repeat, --waits-on or --parent.",
|
|
708
709
|
)
|
|
709
710
|
.replace(
|
|
710
711
|
/^This connection was allowed to read only\..*$/s,
|
|
@@ -785,6 +786,10 @@ const DETAIL = {
|
|
|
785
786
|
" notes, priority, owner, others on it, checklist and due offset fill what",
|
|
786
787
|
" the flags don't. Leave out TITLE to take the template's.",
|
|
787
788
|
"",
|
|
789
|
+
" --parent TASK adds it inside that task as a subtask: a whole task with its",
|
|
790
|
+
" own owner and date, shown in the parent's details rather than on the",
|
|
791
|
+
" board. It goes in the parent's project; subtasks go one level deep.",
|
|
792
|
+
"",
|
|
788
793
|
" --due pilot takes the earliest day the owner's queue suggests: their open",
|
|
789
794
|
" work due first, their weekly pace, time away, what it waits on and how",
|
|
790
795
|
" long their similar tasks took. It needs an owner (--assignee none is",
|
|
@@ -2288,6 +2293,7 @@ export async function run(argv, io = {}) {
|
|
|
2288
2293
|
),
|
|
2289
2294
|
];
|
|
2290
2295
|
if (t.alsoOn?.length) lines.push(`Also on it: ${t.alsoOn.join(", ")}.`);
|
|
2296
|
+
if (t.subtaskOf) lines.push(`Subtask of ${t.subtaskOf.key ?? t.subtaskOf.id} ${t.subtaskOf.title}.`);
|
|
2291
2297
|
if (t.repeats) lines.push(`Repeats ${REPEAT_WORD[t.repeats] ?? t.repeats}.`);
|
|
2292
2298
|
for (const [name, v] of Object.entries(t.fields ?? {}))
|
|
2293
2299
|
lines.push(`${name}: ${Array.isArray(v) ? v.join(", ") : v === true ? "yes" : v}`);
|
|
@@ -2296,6 +2302,15 @@ export async function run(argv, io = {}) {
|
|
|
2296
2302
|
if (t.description) lines.push("", t.description);
|
|
2297
2303
|
if (t.checklist.length)
|
|
2298
2304
|
lines.push("", ...t.checklist.map((c) => ` [${c.done ? "x" : " "}] ${c.step}`));
|
|
2305
|
+
if (t.subtasks?.length)
|
|
2306
|
+
lines.push(
|
|
2307
|
+
"",
|
|
2308
|
+
`Subtasks (${t.subtasks.filter((x) => x.status === "done").length}/${t.subtasks.length} done):`,
|
|
2309
|
+
...t.subtasks.map(
|
|
2310
|
+
(x) =>
|
|
2311
|
+
` [${x.status === "done" ? "x" : " "}] ${x.key ?? x.id} ${x.title}${x.assignee ? `, ${x.assignee}` : ""}${x.due ? `, due ${x.due}` : ""}`,
|
|
2312
|
+
),
|
|
2313
|
+
);
|
|
2299
2314
|
if (t.waitingOn.length)
|
|
2300
2315
|
lines.push(
|
|
2301
2316
|
"",
|
|
@@ -2531,9 +2546,12 @@ export async function run(argv, io = {}) {
|
|
|
2531
2546
|
const { positionals, values, harbor } = parse(
|
|
2532
2547
|
spec(
|
|
2533
2548
|
["over-limit", "dry-run", "literal"],
|
|
2534
|
-
["project", "assignee", "also", "due", "priority", "status", "comment", "template"],
|
|
2549
|
+
["project", "assignee", "also", "due", "priority", "status", "comment", "template", "parent"],
|
|
2535
2550
|
),
|
|
2536
2551
|
);
|
|
2552
|
+
// A subtask goes in its parent's project.
|
|
2553
|
+
if (values.parent && !values.project)
|
|
2554
|
+
values.project = (await client.call("get_task", { harbor, task: values.parent })).projectId;
|
|
2537
2555
|
// --template with no title: the template's own title.
|
|
2538
2556
|
const fromTemplate = values.template !== undefined && positionals.length === 0;
|
|
2539
2557
|
// Several at once: one title a line on stdin, with no title (or -) after add.
|
|
@@ -2636,6 +2654,7 @@ export async function run(argv, io = {}) {
|
|
|
2636
2654
|
suggestedDue: duePilot(values.due),
|
|
2637
2655
|
priority: values.priority ?? words.priority?.value,
|
|
2638
2656
|
status: values.status === undefined ? undefined : statusWord(values.status),
|
|
2657
|
+
parent: values.parent,
|
|
2639
2658
|
overLimit: values["over-limit"] || undefined,
|
|
2640
2659
|
})
|
|
2641
2660
|
.catch((err) => {
|
|
@@ -2761,6 +2780,7 @@ export async function run(argv, io = {}) {
|
|
|
2761
2780
|
"notes",
|
|
2762
2781
|
"repeat",
|
|
2763
2782
|
"waits-on",
|
|
2783
|
+
"parent",
|
|
2764
2784
|
],
|
|
2765
2785
|
),
|
|
2766
2786
|
);
|
|
@@ -2791,12 +2811,18 @@ export async function run(argv, io = {}) {
|
|
|
2791
2811
|
.split(",")
|
|
2792
2812
|
.map((x) => x.trim())
|
|
2793
2813
|
.filter(Boolean),
|
|
2814
|
+
parent: values.parent,
|
|
2794
2815
|
confirmed: values.confirm || undefined,
|
|
2795
2816
|
overLimit: values["over-limit"] || undefined,
|
|
2796
2817
|
},
|
|
2797
2818
|
(r) =>
|
|
2798
2819
|
[
|
|
2799
2820
|
`Updated: ${taskLine(r.updated, opts)}`,
|
|
2821
|
+
values.parent !== undefined
|
|
2822
|
+
? r.updated.subtaskOf
|
|
2823
|
+
? `A subtask of ${r.updated.subtaskOf}.`
|
|
2824
|
+
: "A task of its own now."
|
|
2825
|
+
: null,
|
|
2800
2826
|
values.also !== undefined
|
|
2801
2827
|
? r.updated.alsoOn?.length
|
|
2802
2828
|
? `Also on it: ${r.updated.alsoOn.join(", ")}.`
|