@harborppm/cli 0.93.0 → 0.95.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 +150 -14
- package/lib/cli.mjs +27 -3
- package/package.json +1 -1
package/dist/app.mjs
CHANGED
|
@@ -2879,9 +2879,10 @@ function tideForecast(project, tasks, now, opts = {}) {
|
|
|
2879
2879
|
};
|
|
2880
2880
|
const weekly = weeklyFinished(tasks, project.startDate, now);
|
|
2881
2881
|
const finished = weekly.reduce((a, b) => a + b, 0);
|
|
2882
|
-
if (finished
|
|
2882
|
+
if (finished < 3) return {
|
|
2883
2883
|
kind: "no-pace",
|
|
2884
|
-
remaining
|
|
2884
|
+
remaining,
|
|
2885
|
+
finished
|
|
2885
2886
|
};
|
|
2886
2887
|
const today = todayISO(now);
|
|
2887
2888
|
const base = noon$3(today);
|
|
@@ -2947,11 +2948,11 @@ function tideQuiet(tide) {
|
|
|
2947
2948
|
case "empty": return "No tasks yet, so there's nothing to forecast.";
|
|
2948
2949
|
case "done": return "Every task is done.";
|
|
2949
2950
|
case "paused": return "Paused, so its pace says nothing about when it ends.";
|
|
2950
|
-
case "no-pace": return `Nothing finished in the last ${6} weeks, so there's no pace to forecast from yet.`;
|
|
2951
|
+
case "no-pace": return tide.finished === 0 ? `Nothing finished in the last ${6} weeks, so there's no pace to forecast from yet.` : `${tide.finished === 1 ? "One task has" : `${tide.finished} tasks have`} finished so far. Dates come once ${3} have, so they rest on a real pace.`;
|
|
2951
2952
|
default: return null;
|
|
2952
2953
|
}
|
|
2953
2954
|
}
|
|
2954
|
-
var TIDE_WEEKS, TRIALS, MAX_WEEKS, ROUGH_BELOW, noon$3;
|
|
2955
|
+
var TIDE_WEEKS, TRIALS, MAX_WEEKS, ROUGH_BELOW, TIDE_MIN_FINISHED, noon$3;
|
|
2955
2956
|
var init_tide = __esmMin((() => {
|
|
2956
2957
|
init_date_fns();
|
|
2957
2958
|
init_format();
|
|
@@ -2961,6 +2962,7 @@ var init_tide = __esmMin((() => {
|
|
|
2961
2962
|
TRIALS = 1e3;
|
|
2962
2963
|
MAX_WEEKS = 104;
|
|
2963
2964
|
ROUGH_BELOW = 5;
|
|
2965
|
+
TIDE_MIN_FINISHED = 3;
|
|
2964
2966
|
noon$3 = (iso) => /* @__PURE__ */ new Date(`${iso.slice(0, 10)}T12:00:00`);
|
|
2965
2967
|
}));
|
|
2966
2968
|
|
|
@@ -5777,7 +5779,7 @@ function matches$1(row, f) {
|
|
|
5777
5779
|
if (f.hideDone && row.health === "done") return false;
|
|
5778
5780
|
return true;
|
|
5779
5781
|
}
|
|
5780
|
-
function milestoneOf(m, today) {
|
|
5782
|
+
function milestoneOf$1(m, today) {
|
|
5781
5783
|
return {
|
|
5782
5784
|
id: m.id,
|
|
5783
5785
|
name: m.name,
|
|
@@ -5816,7 +5818,7 @@ function roadmapRows(data, portfolioId, filter, collapsed, today) {
|
|
|
5816
5818
|
baselineEnd: null,
|
|
5817
5819
|
slip: null,
|
|
5818
5820
|
waitsOn: blockerIds(program).filter((id) => programIds.has(id)),
|
|
5819
|
-
milestones: data.milestones.filter((m) => m.programId === program.id && !(filter.hideDone && m.done)).sort((a, b) => a.dueDate.localeCompare(b.dueDate)).map((m) => milestoneOf(m, today)),
|
|
5821
|
+
milestones: data.milestones.filter((m) => m.programId === program.id && !(filter.hideDone && m.done)).sort((a, b) => a.dueDate.localeCompare(b.dueDate)).map((m) => milestoneOf$1(m, today)),
|
|
5820
5822
|
projectCount: projects.length
|
|
5821
5823
|
});
|
|
5822
5824
|
if (collapsed.has(program.id)) continue;
|
|
@@ -6293,7 +6295,7 @@ function tideLine(data, project, today) {
|
|
|
6293
6295
|
const tasks = tasksOf(data.tasks, project.id);
|
|
6294
6296
|
const tide = tideForecast(project, tasks, parseIso(`${today}T12:00:00`), tideOptions(data));
|
|
6295
6297
|
if (tide.kind === "no-pace") return {
|
|
6296
|
-
text: "No forecast yet: nothing finished in the last few weeks",
|
|
6298
|
+
text: tide.finished === 0 ? "No forecast yet: nothing finished in the last few weeks" : `No forecast yet: ${tide.finished} finished, dates come at 3`,
|
|
6297
6299
|
late: false
|
|
6298
6300
|
};
|
|
6299
6301
|
if (tide.kind !== "forecast") return null;
|
|
@@ -10028,19 +10030,21 @@ function mayChangeMessage(message, personId, role, action) {
|
|
|
10028
10030
|
* What a person's Inbox says about project messages since `since`: each
|
|
10029
10031
|
* message that @mentions them, and for each project they've written in, the
|
|
10030
10032
|
* messages others posted after their last one (a mention among them is
|
|
10031
|
-
* already its own line).
|
|
10033
|
+
* already its own line). A finished project's replies are left out, but a
|
|
10034
|
+
* mention there still reaches you: a wrap-up "thanks @Dana" is meant to.
|
|
10032
10035
|
*/
|
|
10033
10036
|
function messageLines(data, personId, since) {
|
|
10034
10037
|
const done = new Set(data.projects.filter((p) => p.health === "done").map((p) => p.id));
|
|
10035
10038
|
const byProject = /* @__PURE__ */ new Map();
|
|
10036
10039
|
for (const r of data.reflections ?? []) {
|
|
10037
|
-
if (!isMessage(r)
|
|
10040
|
+
if (!isMessage(r)) continue;
|
|
10038
10041
|
const list = byProject.get(r.projectId);
|
|
10039
10042
|
if (list) list.push(r);
|
|
10040
10043
|
else byProject.set(r.projectId, [r]);
|
|
10041
10044
|
}
|
|
10042
10045
|
const out = [];
|
|
10043
|
-
for (const list of byProject
|
|
10046
|
+
for (const [projectId, list] of byProject) {
|
|
10047
|
+
const finished = done.has(projectId);
|
|
10044
10048
|
list.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
10045
10049
|
let joined = false;
|
|
10046
10050
|
let replies = [];
|
|
@@ -10059,7 +10063,7 @@ function messageLines(data, personId, since) {
|
|
|
10059
10063
|
});
|
|
10060
10064
|
continue;
|
|
10061
10065
|
}
|
|
10062
|
-
if (joined) replies.push(m);
|
|
10066
|
+
if (joined && !finished) replies.push(m);
|
|
10063
10067
|
}
|
|
10064
10068
|
const latest = replies.at(-1);
|
|
10065
10069
|
if (latest) out.push({
|
|
@@ -14117,6 +14121,107 @@ function applyDock(actions, projectId, changes) {
|
|
|
14117
14121
|
return made;
|
|
14118
14122
|
}
|
|
14119
14123
|
|
|
14124
|
+
// src/lib/task-milestones.ts
|
|
14125
|
+
/** The milestone a task is tied to, or null when it isn't (or the tie no longer holds). */
|
|
14126
|
+
function milestoneOf(task, milestones, projects) {
|
|
14127
|
+
if (!task.milestoneId) return null;
|
|
14128
|
+
const m = byId(milestones).get(task.milestoneId);
|
|
14129
|
+
if (!m) return null;
|
|
14130
|
+
const project = byId(projects).get(task.projectId);
|
|
14131
|
+
return project && project.programId === m.programId ? m : null;
|
|
14132
|
+
}
|
|
14133
|
+
/** For each milestone with tasks tied to it, how many there are and how many are done. One pass. */
|
|
14134
|
+
function milestoneProgress(doc) {
|
|
14135
|
+
const out = /* @__PURE__ */ new Map();
|
|
14136
|
+
if (doc.milestones.length === 0) return out;
|
|
14137
|
+
for (const t of doc.tasks) {
|
|
14138
|
+
if (!t.milestoneId) continue;
|
|
14139
|
+
const m = milestoneOf(t, doc.milestones, doc.projects);
|
|
14140
|
+
if (!m) continue;
|
|
14141
|
+
const p = out.get(m.id) ?? {
|
|
14142
|
+
done: 0,
|
|
14143
|
+
total: 0
|
|
14144
|
+
};
|
|
14145
|
+
p.total++;
|
|
14146
|
+
if (t.status === "done") p.done++;
|
|
14147
|
+
out.set(m.id, p);
|
|
14148
|
+
}
|
|
14149
|
+
return out;
|
|
14150
|
+
}
|
|
14151
|
+
/** Every project it waits on is done (no waits, or a project gone, count as clear). */
|
|
14152
|
+
function waitsClear(m, projects) {
|
|
14153
|
+
const index = byId(projects);
|
|
14154
|
+
return blockerIds(m).every((id) => {
|
|
14155
|
+
const p = index.get(id);
|
|
14156
|
+
return !p || p.health === "done";
|
|
14157
|
+
});
|
|
14158
|
+
}
|
|
14159
|
+
/**
|
|
14160
|
+
* The document after a write, with milestones moved by what happened to their
|
|
14161
|
+
* tasks: a milestone that lost an open task (finished, deleted, untied or
|
|
14162
|
+
* moved away) and now has all its tasks done is reached; a reached one that
|
|
14163
|
+
* gained an open task (reopened, tied, added or moved in) is open again. The
|
|
14164
|
+
* same document back when nothing moved. Only tasks this write changed are
|
|
14165
|
+
* looked at, then one pass counts the milestones they touch.
|
|
14166
|
+
*/
|
|
14167
|
+
function followMilestones(before, after) {
|
|
14168
|
+
if (after.tasks === before.tasks || after.milestones.length === 0) return after;
|
|
14169
|
+
const was = byId(before.tasks);
|
|
14170
|
+
const tie = (t, doc) => milestoneOf(t, doc.milestones, doc.projects)?.id ?? null;
|
|
14171
|
+
const lost = /* @__PURE__ */ new Set();
|
|
14172
|
+
const gained = /* @__PURE__ */ new Set();
|
|
14173
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14174
|
+
for (const t of after.tasks) {
|
|
14175
|
+
const old = was.get(t.id);
|
|
14176
|
+
if (old) seen.add(t.id);
|
|
14177
|
+
if (old === t) continue;
|
|
14178
|
+
const from = old ? tie(old, before) : null;
|
|
14179
|
+
const to = tie(t, after);
|
|
14180
|
+
const wasOpen = old ? old.status !== "done" : false;
|
|
14181
|
+
const isOpen = t.status !== "done";
|
|
14182
|
+
if (from && wasOpen && (from !== to || !isOpen)) lost.add(from);
|
|
14183
|
+
if (to && isOpen && (from !== to || !wasOpen)) gained.add(to);
|
|
14184
|
+
}
|
|
14185
|
+
if (seen.size < before.tasks.length) for (const old of before.tasks) {
|
|
14186
|
+
if (seen.has(old.id) || old.status === "done") continue;
|
|
14187
|
+
const from = tie(old, before);
|
|
14188
|
+
if (from) lost.add(from);
|
|
14189
|
+
}
|
|
14190
|
+
if (lost.size === 0 && gained.size === 0) return after;
|
|
14191
|
+
const progress = milestoneProgress(after);
|
|
14192
|
+
let changed = false;
|
|
14193
|
+
const milestones = after.milestones.map((m) => {
|
|
14194
|
+
if (!follows(m)) return m;
|
|
14195
|
+
const p = progress.get(m.id);
|
|
14196
|
+
if (!m.done && lost.has(m.id) && p && p.total > 0 && p.done === p.total) {
|
|
14197
|
+
if (!waitsClear(m, after.projects)) return m;
|
|
14198
|
+
changed = true;
|
|
14199
|
+
return {
|
|
14200
|
+
...m,
|
|
14201
|
+
done: true
|
|
14202
|
+
};
|
|
14203
|
+
}
|
|
14204
|
+
if (m.done && gained.has(m.id)) {
|
|
14205
|
+
changed = true;
|
|
14206
|
+
return {
|
|
14207
|
+
...m,
|
|
14208
|
+
done: false
|
|
14209
|
+
};
|
|
14210
|
+
}
|
|
14211
|
+
return m;
|
|
14212
|
+
});
|
|
14213
|
+
return changed ? {
|
|
14214
|
+
...after,
|
|
14215
|
+
milestones
|
|
14216
|
+
} : after;
|
|
14217
|
+
}
|
|
14218
|
+
var follows;
|
|
14219
|
+
var init_task_milestones = __esmMin((() => {
|
|
14220
|
+
init_blockers();
|
|
14221
|
+
init_indexes();
|
|
14222
|
+
follows = (m) => !m.approverId;
|
|
14223
|
+
}));
|
|
14224
|
+
|
|
14120
14225
|
// src/lib/time-limits.ts
|
|
14121
14226
|
var MAX_ENTRY_MINUTES;
|
|
14122
14227
|
var init_time_limits = __esmMin((() => {
|
|
@@ -14124,6 +14229,7 @@ var init_time_limits = __esmMin((() => {
|
|
|
14124
14229
|
}));
|
|
14125
14230
|
|
|
14126
14231
|
// src/lib/time-log.ts
|
|
14232
|
+
init_task_milestones();
|
|
14127
14233
|
/** "1h 30m", "45m", "12h". */
|
|
14128
14234
|
function formatMinutes(total) {
|
|
14129
14235
|
const n = Math.max(0, Math.round(total));
|
|
@@ -21943,7 +22049,7 @@ const HARBOR_TOOLS = [
|
|
|
21943
22049
|
tool({
|
|
21944
22050
|
name: "get_task",
|
|
21945
22051
|
title: "Task details",
|
|
21946
|
-
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,
|
|
22052
|
+
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), the milestone it's tied to (milestone: its due day, whether it's reached, and how many of its tasks are done of all tied to it), and its url on the website." + DATA_NOTE,
|
|
21947
22053
|
input: object({
|
|
21948
22054
|
harbor: harborArg,
|
|
21949
22055
|
task: string().min(1).max(100).describe("Task id or key (\"RR-42\"), from search_tasks or my_work.")
|
|
@@ -21997,6 +22103,12 @@ const HARBOR_TOOLS = [
|
|
|
21997
22103
|
waitingOn: blockersOf(t, data.tasks).map(brief),
|
|
21998
22104
|
waitedOnBy: waiters.map(brief),
|
|
21999
22105
|
...((parent) => parent ? { subtaskOf: brief(parent) } : {})(parentOf(t, data.tasks)),
|
|
22106
|
+
...((m) => m ? { milestone: {
|
|
22107
|
+
name: m.name,
|
|
22108
|
+
due: dayOf(m.dueDate),
|
|
22109
|
+
reached: m.done,
|
|
22110
|
+
...milestoneProgress(data).get(m.id)
|
|
22111
|
+
} } : {})(milestoneOf(t, data.milestones, data.projects)),
|
|
22000
22112
|
...((subs) => subs.length ? { subtasks: subs.map((x) => ({
|
|
22001
22113
|
...brief(x),
|
|
22002
22114
|
assignee: nameOf$2(people, x.assigneeId),
|
|
@@ -24507,6 +24619,7 @@ function nextOccurrence(task, id, today, createdAt, siblings = []) {
|
|
|
24507
24619
|
id,
|
|
24508
24620
|
status: "todo",
|
|
24509
24621
|
dueDate,
|
|
24622
|
+
milestoneId: null,
|
|
24510
24623
|
...task.startDate ? { startDate: lead != null && lead >= 0 ? format(addDays(/* @__PURE__ */ new Date(`${dueDate}T12:00:00`), -lead), "yyyy-MM-dd") : null } : {},
|
|
24511
24624
|
createdAt,
|
|
24512
24625
|
completedAt: null,
|
|
@@ -25958,10 +26071,10 @@ function createAppStore(onNotice = () => {}) {
|
|
|
25958
26071
|
};
|
|
25959
26072
|
const write = (partial) => {
|
|
25960
26073
|
const before = get();
|
|
25961
|
-
guardedSet((s) => stampHealth(numberNewTasks(s, {
|
|
26074
|
+
guardedSet((s) => stampHealth(followMilestones(s, numberNewTasks(s, {
|
|
25962
26075
|
...s,
|
|
25963
26076
|
...partial
|
|
25964
|
-
})));
|
|
26077
|
+
}))));
|
|
25965
26078
|
undo = nextStep(undo, before, get(), Date.now());
|
|
25966
26079
|
};
|
|
25967
26080
|
const api = {
|
|
@@ -26027,6 +26140,7 @@ var init_store = __esmMin((() => {
|
|
|
26027
26140
|
init_vanilla();
|
|
26028
26141
|
init_derived_health();
|
|
26029
26142
|
init_task_keys();
|
|
26143
|
+
init_task_milestones();
|
|
26030
26144
|
init_payload();
|
|
26031
26145
|
init_undo();
|
|
26032
26146
|
init_view_only();
|
|
@@ -31307,6 +31421,7 @@ init_recurrence();
|
|
|
31307
31421
|
init_task_people();
|
|
31308
31422
|
init_search();
|
|
31309
31423
|
init_swatches();
|
|
31424
|
+
init_task_milestones();
|
|
31310
31425
|
const FIELDS = [
|
|
31311
31426
|
{
|
|
31312
31427
|
field: "title",
|
|
@@ -31356,6 +31471,7 @@ function TaskPanel({ taskId, width, height, go, active = true, onClose }) {
|
|
|
31356
31471
|
const { store, today } = useDeps();
|
|
31357
31472
|
const tasks = useHarbor((s) => s.tasks);
|
|
31358
31473
|
const projects = useHarbor((s) => s.projects);
|
|
31474
|
+
const milestones = useHarbor((s) => s.milestones);
|
|
31359
31475
|
const people = useHarbor((s) => s.people);
|
|
31360
31476
|
const comments = useHarbor((s) => s.comments);
|
|
31361
31477
|
const reflections = useHarbor((s) => s.reflections);
|
|
@@ -31422,6 +31538,12 @@ function TaskPanel({ taskId, width, height, go, active = true, onClose }) {
|
|
|
31422
31538
|
const mine = comments.filter((c) => c.taskId === task.id && c.authorId === me);
|
|
31423
31539
|
const blockers = blockersOf(task, tasks);
|
|
31424
31540
|
const subtaskOf = parentOf(task, tasks);
|
|
31541
|
+
const milestone = milestoneOf(task, milestones, projects);
|
|
31542
|
+
const toward = milestone ? milestoneProgress({
|
|
31543
|
+
tasks,
|
|
31544
|
+
milestones,
|
|
31545
|
+
projects
|
|
31546
|
+
}).get(milestone.id) : void 0;
|
|
31425
31547
|
const subs = subtasksOf(task, tasks);
|
|
31426
31548
|
const done = () => setEditing(null);
|
|
31427
31549
|
const nameOf = (id) => people.find((p) => p.id === id)?.name;
|
|
@@ -31786,6 +31908,20 @@ function TaskPanel({ taskId, width, height, go, active = true, onClose }) {
|
|
|
31786
31908
|
subtaskOf.title
|
|
31787
31909
|
]
|
|
31788
31910
|
}) : null,
|
|
31911
|
+
milestone ? /* @__PURE__ */ jsxs(Text, {
|
|
31912
|
+
color: theme.muted,
|
|
31913
|
+
wrap: "truncate-end",
|
|
31914
|
+
children: [
|
|
31915
|
+
" ",
|
|
31916
|
+
"Toward ",
|
|
31917
|
+
milestone.name,
|
|
31918
|
+
" (",
|
|
31919
|
+
milestone.dueDate.slice(0, 10),
|
|
31920
|
+
"):",
|
|
31921
|
+
" ",
|
|
31922
|
+
milestone.done ? "reached" : `${toward?.done ?? 0}/${toward?.total ?? 0} of its tasks done`
|
|
31923
|
+
]
|
|
31924
|
+
}) : null,
|
|
31789
31925
|
subs.length > 0 ? /* @__PURE__ */ jsxs(Text, {
|
|
31790
31926
|
color: theme.muted,
|
|
31791
31927
|
children: [
|
package/lib/cli.mjs
CHANGED
|
@@ -170,6 +170,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
170
170
|
[--priority low|medium|high|urgent]
|
|
171
171
|
[--status S] [--comment TEXT] [--over-limit] [--dry-run]
|
|
172
172
|
[--literal] [--template NAME] [--parent TASK]
|
|
173
|
+
[--milestone NAME]
|
|
173
174
|
reads plain words in TITLE as the
|
|
174
175
|
website does: @name, by friday,
|
|
175
176
|
!high, #project (--literal: keep
|
|
@@ -186,7 +187,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
186
187
|
[--priority P] [--status S] [--notes TEXT]
|
|
187
188
|
[--repeat weekly|biweekly|monthly|never]
|
|
188
189
|
[--waits-on TASK,TASK|none] [--parent TASK|none]
|
|
189
|
-
[--confirm] [--over-limit]
|
|
190
|
+
[--milestone NAME|none] [--confirm] [--over-limit]
|
|
190
191
|
harbor limit PROJECT [COLUMN N|none] how many tasks a board column
|
|
191
192
|
takes at once; the owner sets it
|
|
192
193
|
harbor week [--next|--last|--week DATE] [--review]
|
|
@@ -710,7 +711,7 @@ export function hint(message, project) {
|
|
|
710
711
|
)
|
|
711
712
|
.replace(
|
|
712
713
|
/^Nothing to change: pass a field to update\.$/,
|
|
713
|
-
"Nothing to change. Say what with --title, --assignee, --also, --due, --starts, --priority, --status, --notes, --repeat, --waits-on or --
|
|
714
|
+
"Nothing to change. Say what with --title, --assignee, --also, --due, --starts, --priority, --status, --notes, --repeat, --waits-on, --parent or --milestone.",
|
|
714
715
|
)
|
|
715
716
|
.replace(
|
|
716
717
|
/^This connection was allowed to read only\..*$/s,
|
|
@@ -795,6 +796,11 @@ const DETAIL = {
|
|
|
795
796
|
" own owner and date, shown in the parent's details rather than on the",
|
|
796
797
|
" board. It goes in the parent's project; subtasks go one level deep.",
|
|
797
798
|
"",
|
|
799
|
+
" --milestone NAME ties it to one of the program's milestones: the milestone",
|
|
800
|
+
" is reached once its tasks are done, and opens again when one reopens. The",
|
|
801
|
+
" project's timeline shows each milestone under its tasks. harbor edit TASK",
|
|
802
|
+
" --milestone NAME|none changes or clears it.",
|
|
803
|
+
"",
|
|
798
804
|
" --starts DATE gives it a start day as well as a due day, so the project's",
|
|
799
805
|
" timeline draws it as a bar from one to the other; a start after the due",
|
|
800
806
|
" day comes to the due day. harbor edit TASK --starts DATE|none moves it.",
|
|
@@ -2312,6 +2318,10 @@ export async function run(argv, io = {}) {
|
|
|
2312
2318
|
if (t.alsoOn?.length) lines.push(`Also on it: ${t.alsoOn.join(", ")}.`);
|
|
2313
2319
|
if (t.starts) lines.push(`Starts ${t.starts}${t.due ? `, due ${t.due}` : ""}.`);
|
|
2314
2320
|
if (t.subtaskOf) lines.push(`Subtask of ${t.subtaskOf.key ?? t.subtaskOf.id} ${t.subtaskOf.title}.`);
|
|
2321
|
+
if (t.milestone)
|
|
2322
|
+
lines.push(
|
|
2323
|
+
`Toward the milestone ${t.milestone.name} (${t.milestone.due}): ${t.milestone.reached ? "reached" : `${t.milestone.done} of ${t.milestone.total} of its tasks done`}.`,
|
|
2324
|
+
);
|
|
2315
2325
|
if (t.repeats) lines.push(`Repeats ${REPEAT_WORD[t.repeats] ?? t.repeats}.`);
|
|
2316
2326
|
for (const [name, v] of Object.entries(t.fields ?? {}))
|
|
2317
2327
|
lines.push(`${name}: ${Array.isArray(v) ? v.join(", ") : v === true ? "yes" : v}`);
|
|
@@ -2564,7 +2574,7 @@ export async function run(argv, io = {}) {
|
|
|
2564
2574
|
const { positionals, values, harbor } = parse(
|
|
2565
2575
|
spec(
|
|
2566
2576
|
["over-limit", "dry-run", "literal"],
|
|
2567
|
-
["project", "assignee", "also", "due", "starts", "priority", "status", "comment", "template", "parent"],
|
|
2577
|
+
["project", "assignee", "also", "due", "starts", "priority", "status", "comment", "template", "parent", "milestone"],
|
|
2568
2578
|
),
|
|
2569
2579
|
);
|
|
2570
2580
|
// A subtask goes in its parent's project.
|
|
@@ -2674,6 +2684,7 @@ export async function run(argv, io = {}) {
|
|
|
2674
2684
|
priority: values.priority ?? words.priority?.value,
|
|
2675
2685
|
status: values.status === undefined ? undefined : statusWord(values.status),
|
|
2676
2686
|
parent: values.parent,
|
|
2687
|
+
milestone: values.milestone,
|
|
2677
2688
|
overLimit: values["over-limit"] || undefined,
|
|
2678
2689
|
})
|
|
2679
2690
|
.catch((err) => {
|
|
@@ -2712,6 +2723,8 @@ export async function run(argv, io = {}) {
|
|
|
2712
2723
|
...(dueWords(values.due) ? [`--due ${values.due}: ${dayWithWeekday(dueDate(values.due))}.`] : []),
|
|
2713
2724
|
// Pilot speaks up only when a date was given that the queue says won't hold.
|
|
2714
2725
|
...(values.due ? pilotLines(r, r.created) : []),
|
|
2726
|
+
...(r.created.milestone ? [`Toward the milestone ${r.created.milestone}.`] : []),
|
|
2727
|
+
...(r.milestoneReopened ? [`${r.milestoneReopened} is open again: it has an open task now.`] : []),
|
|
2715
2728
|
].join("\n"),
|
|
2716
2729
|
)
|
|
2717
2730
|
.join("\n"),
|
|
@@ -2777,6 +2790,8 @@ export async function run(argv, io = {}) {
|
|
|
2777
2790
|
[
|
|
2778
2791
|
`${r.updated.title}: ${r.reopened ? "reopened, " : ""}${{ doing: "started", review: "in review", done: "done" }[status]}.`,
|
|
2779
2792
|
r.queuedNext ? `Next one: ${taskLine(r.queuedNext, opts)}` : "",
|
|
2793
|
+
r.milestoneReached ? `That was the last of its tasks: ${r.milestoneReached} is reached.` : "",
|
|
2794
|
+
r.milestoneReopened ? `${r.milestoneReopened} is open again: one of its tasks is open.` : "",
|
|
2780
2795
|
r.projectFinished
|
|
2781
2796
|
? `That was the last open task in ${r.projectFinished}. Mark the project Done in the app (harbor) or on the website.`
|
|
2782
2797
|
: "",
|
|
@@ -2801,6 +2816,7 @@ export async function run(argv, io = {}) {
|
|
|
2801
2816
|
"repeat",
|
|
2802
2817
|
"waits-on",
|
|
2803
2818
|
"parent",
|
|
2819
|
+
"milestone",
|
|
2804
2820
|
],
|
|
2805
2821
|
),
|
|
2806
2822
|
);
|
|
@@ -2833,6 +2849,7 @@ export async function run(argv, io = {}) {
|
|
|
2833
2849
|
.map((x) => x.trim())
|
|
2834
2850
|
.filter(Boolean),
|
|
2835
2851
|
parent: values.parent,
|
|
2852
|
+
milestone: values.milestone,
|
|
2836
2853
|
confirmed: values.confirm || undefined,
|
|
2837
2854
|
overLimit: values["over-limit"] || undefined,
|
|
2838
2855
|
},
|
|
@@ -2849,6 +2866,13 @@ export async function run(argv, io = {}) {
|
|
|
2849
2866
|
? `A subtask of ${r.updated.subtaskOf}.`
|
|
2850
2867
|
: "A task of its own now."
|
|
2851
2868
|
: null,
|
|
2869
|
+
values.milestone !== undefined
|
|
2870
|
+
? r.updated.milestone
|
|
2871
|
+
? `Toward the milestone ${r.updated.milestone}.`
|
|
2872
|
+
: "Not tied to a milestone now."
|
|
2873
|
+
: null,
|
|
2874
|
+
r.milestoneReached ? `That was the last of its tasks: ${r.milestoneReached} is reached.` : null,
|
|
2875
|
+
r.milestoneReopened ? `${r.milestoneReopened} is open again: one of its tasks is open.` : null,
|
|
2852
2876
|
values.also !== undefined
|
|
2853
2877
|
? r.updated.alsoOn?.length
|
|
2854
2878
|
? `Also on it: ${r.updated.alsoOn.join(", ")}.`
|