@harborppm/cli 0.89.0 → 0.91.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 +3 -2
- package/bin/harbor.mjs +3 -2
- package/dist/app.mjs +80 -22
- package/lib/cli.mjs +17 -3
- package/lib/exit.mjs +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,7 +166,8 @@ harbor project "NAME" --program P [--lead me|NAME] [--starts DATE]
|
|
|
166
166
|
they really finished in that project
|
|
167
167
|
harbor add "TITLE" --project P [--assignee me|NAME] [--also "NAME, NAME"] [--due DATE|pilot]
|
|
168
168
|
[--priority low|medium|high|urgent] [--status S] [--comment TEXT]
|
|
169
|
-
[--over-limit] [--literal]
|
|
169
|
+
[--starts DATE] [--over-limit] [--literal]
|
|
170
|
+
no TITLE: one title a line from stdin
|
|
170
171
|
--due pilot takes the earliest day the owner's queue
|
|
171
172
|
suggests (their open work due first, weekly pace, time
|
|
172
173
|
away, what it waits on, how long their similar tasks
|
|
@@ -174,7 +175,7 @@ harbor add "TITLE" --project P [--assignee me|NAME] [--also "NAME, NAME"] [--due
|
|
|
174
175
|
their next working day. A date that won't hold says so
|
|
175
176
|
harbor start TASK | submit TASK | done TASK [--confirm] [--over-limit]
|
|
176
177
|
harbor edit TASK [--title T] [--assignee me|none|NAME] [--also "NAME, NAME"|none]
|
|
177
|
-
[--due DATE|pilot|none]
|
|
178
|
+
[--due DATE|pilot|none] [--starts DATE|none]
|
|
178
179
|
[--priority P] [--status S] [--notes TEXT]
|
|
179
180
|
[--repeat weekly|biweekly|monthly|never]
|
|
180
181
|
[--waits-on TASK,TASK|none] [--confirm] [--over-limit]
|
package/bin/harbor.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { VERSION, run } from "../lib/cli.mjs";
|
|
3
|
+
import { exitWhenWritten } from "../lib/exit.mjs";
|
|
3
4
|
import { HarborError } from "../lib/mcp.mjs";
|
|
4
5
|
import { jsonText, plain } from "../lib/plain.mjs";
|
|
5
6
|
import { updateNotice } from "../lib/update.mjs";
|
|
@@ -19,7 +20,7 @@ const tell = async () => {
|
|
|
19
20
|
run(argv).then(
|
|
20
21
|
async (code) => {
|
|
21
22
|
await tell();
|
|
22
|
-
|
|
23
|
+
await exitWhenWritten(code);
|
|
23
24
|
},
|
|
24
25
|
async (err) => {
|
|
25
26
|
const known =
|
|
@@ -33,6 +34,6 @@ run(argv).then(
|
|
|
33
34
|
if (argv.includes("--json")) process.stdout.write(`${jsonText({ error: message })}\n`);
|
|
34
35
|
else process.stderr.write(`${plain(message)}\n`);
|
|
35
36
|
await tell();
|
|
36
|
-
|
|
37
|
+
await exitWhenWritten(1);
|
|
37
38
|
},
|
|
38
39
|
);
|
package/dist/app.mjs
CHANGED
|
@@ -3670,8 +3670,8 @@ init_date_text();
|
|
|
3670
3670
|
init_format();
|
|
3671
3671
|
init_indexes();
|
|
3672
3672
|
init_premortem();
|
|
3673
|
-
const DAY = (iso) => parseIso(iso.slice(0, 10));
|
|
3674
|
-
const span$1 = (a, b) => differenceInCalendarDays(DAY(b), DAY(a));
|
|
3673
|
+
const DAY$1 = (iso) => parseIso(iso.slice(0, 10));
|
|
3674
|
+
const span$1 = (a, b) => differenceInCalendarDays(DAY$1(b), DAY$1(a));
|
|
3675
3675
|
/** Projects marked Done that have tasks to copy, most recently finished first. */
|
|
3676
3676
|
function finishedProjects(data) {
|
|
3677
3677
|
return data.projects.filter((p) => p.health === "done" && tasksOf(data.tasks, p.id).length > 0).map((p) => ({
|
|
@@ -3762,7 +3762,7 @@ function reckoningLine(r) {
|
|
|
3762
3762
|
}
|
|
3763
3763
|
/** The new project's end date, as long after its start as the old one really ran. */
|
|
3764
3764
|
function reckonedEnd(r, startDate) {
|
|
3765
|
-
return todayISO(addDays(DAY(startDate), r.took));
|
|
3765
|
+
return todayISO(addDays(DAY$1(startDate), r.took));
|
|
3766
3766
|
}
|
|
3767
3767
|
/** Puts the reckoning's tasks and risks into the new project, dated from its start. */
|
|
3768
3768
|
function applyReckoning(actions, projectId, r, startDate) {
|
|
@@ -3773,7 +3773,7 @@ function applyReckoning(actions, projectId, r, startDate) {
|
|
|
3773
3773
|
status: "todo",
|
|
3774
3774
|
priority: t.priority,
|
|
3775
3775
|
assigneeId: null,
|
|
3776
|
-
dueDate: todayISO(addDays(DAY(startDate), t.dueIn))
|
|
3776
|
+
dueDate: todayISO(addDays(DAY$1(startDate), t.dueIn))
|
|
3777
3777
|
}, {
|
|
3778
3778
|
allowOverCap: true,
|
|
3779
3779
|
allowBlocked: true
|
|
@@ -13990,6 +13990,33 @@ function linkedProjects(kr, projects) {
|
|
|
13990
13990
|
if (ids.size === 0) return [];
|
|
13991
13991
|
return projects.filter((p) => ids.has(p.id) || ids.has(p.programId));
|
|
13992
13992
|
}
|
|
13993
|
+
/**
|
|
13994
|
+
* The tasks a work key result counts: every task in the projects it names
|
|
13995
|
+
* (directly or through their program), and each task it names on its own,
|
|
13996
|
+
* each counted once however many ways it's linked.
|
|
13997
|
+
*/
|
|
13998
|
+
function workCount(kr, work) {
|
|
13999
|
+
let done = 0;
|
|
14000
|
+
let all = 0;
|
|
14001
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14002
|
+
const count = (t) => {
|
|
14003
|
+
if (seen.has(t.id)) return;
|
|
14004
|
+
seen.add(t.id);
|
|
14005
|
+
all++;
|
|
14006
|
+
if (t.status === "done") done++;
|
|
14007
|
+
};
|
|
14008
|
+
const projects = linkedProjects(kr, work.projects);
|
|
14009
|
+
for (const p of projects) for (const t of tasksOf(work.tasks, p.id)) count(t);
|
|
14010
|
+
const tasks = byId(work.tasks);
|
|
14011
|
+
for (const id of kr.linkIds ?? []) {
|
|
14012
|
+
const t = tasks.get(id);
|
|
14013
|
+
if (t) count(t);
|
|
14014
|
+
}
|
|
14015
|
+
return {
|
|
14016
|
+
done,
|
|
14017
|
+
all
|
|
14018
|
+
};
|
|
14019
|
+
}
|
|
13993
14020
|
/** How far along one key result is, 0 to 1. */
|
|
13994
14021
|
function keyResultProgress(kr, work) {
|
|
13995
14022
|
if (kr.kind === "number") {
|
|
@@ -13998,12 +14025,7 @@ function keyResultProgress(kr, work) {
|
|
|
13998
14025
|
if (span === 0) return (kr.current ?? start) >= (kr.target ?? 0) ? 1 : 0;
|
|
13999
14026
|
return clamp(((kr.current ?? start) - start) / span);
|
|
14000
14027
|
}
|
|
14001
|
-
|
|
14002
|
-
let all = 0;
|
|
14003
|
-
for (const p of linkedProjects(kr, work.projects)) for (const t of tasksOf(work.tasks, p.id)) {
|
|
14004
|
-
all++;
|
|
14005
|
-
if (t.status === "done") done++;
|
|
14006
|
-
}
|
|
14028
|
+
const { done, all } = workCount(kr, work);
|
|
14007
14029
|
return all === 0 ? 0 : done / all;
|
|
14008
14030
|
}
|
|
14009
14031
|
/** How far along a goal is: the average of its key results, 0 to 1. */
|
|
@@ -14012,10 +14034,12 @@ function goalProgress(goal, work) {
|
|
|
14012
14034
|
if (krs.length === 0) return 0;
|
|
14013
14035
|
return krs.reduce((sum, kr) => sum + keyResultProgress(kr, work), 0) / krs.length;
|
|
14014
14036
|
}
|
|
14015
|
-
/**
|
|
14037
|
+
/** The day a goal's clock starts: its start day, or the day it was set. */
|
|
14038
|
+
const goalStart = (goal) => goal.start || goal.createdAt.slice(0, 10);
|
|
14039
|
+
/** How much of the goal's time has gone by, 0 to 1, from its start day to its due day. */
|
|
14016
14040
|
function timeGone(goal, today) {
|
|
14017
14041
|
if (!goal.due) return null;
|
|
14018
|
-
const start = Date.parse(`${goal
|
|
14042
|
+
const start = Date.parse(`${goalStart(goal)}T00:00:00Z`);
|
|
14019
14043
|
const end = Date.parse(`${goal.due}T00:00:00Z`);
|
|
14020
14044
|
const now = Date.parse(`${today}T00:00:00Z`);
|
|
14021
14045
|
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
|
@@ -14033,6 +14057,7 @@ function goalStatus(progress, goal, today) {
|
|
|
14033
14057
|
const gone = timeGone(goal, today);
|
|
14034
14058
|
if (gone == null) return progress > 0 ? "on-track" : "not-started";
|
|
14035
14059
|
if (goal.due && today > goal.due) return "off-track";
|
|
14060
|
+
if (today < goalStart(goal)) return "not-started";
|
|
14036
14061
|
if (progress === 0 && gone < .1) return "not-started";
|
|
14037
14062
|
const behind = gone - progress;
|
|
14038
14063
|
if (behind <= .1) return "on-track";
|
|
@@ -14045,12 +14070,8 @@ function keyResultText(kr, work) {
|
|
|
14045
14070
|
const unit = kr.unit ? ` ${kr.unit}` : "";
|
|
14046
14071
|
return `${fmt(kr.current ?? kr.start ?? 0)} of ${fmt(kr.target ?? 0)}${unit}`;
|
|
14047
14072
|
}
|
|
14048
|
-
|
|
14049
|
-
|
|
14050
|
-
for (const p of linkedProjects(kr, work.projects)) for (const t of tasksOf(work.tasks, p.id)) {
|
|
14051
|
-
all++;
|
|
14052
|
-
if (t.status === "done") done++;
|
|
14053
|
-
}
|
|
14073
|
+
if (!kr.linkIds?.length) return "Nothing linked yet";
|
|
14074
|
+
const { done, all } = workCount(kr, work);
|
|
14054
14075
|
return all === 0 ? "No tasks yet" : `${done} of ${all} tasks done`;
|
|
14055
14076
|
}
|
|
14056
14077
|
const fmt = (n) => Number.isInteger(n) ? n.toLocaleString("en-US") : String(Math.round(n * 100) / 100);
|
|
@@ -19723,6 +19744,18 @@ function undatedText(pole) {
|
|
|
19723
19744
|
return n === 1 ? "1 open task has no due date, so it can't hold the date." : `${n} open tasks have no due date, so they can't hold the date.`;
|
|
19724
19745
|
}
|
|
19725
19746
|
|
|
19747
|
+
// src/lib/raid-register.ts
|
|
19748
|
+
init_raid();
|
|
19749
|
+
/**
|
|
19750
|
+
* What agents and `harbor risks` list unless asked for everything: anything
|
|
19751
|
+
* still needing attention, plus decisions that stand. A decision is decided
|
|
19752
|
+
* once and then holds, so leaving it out the moment it's settled read as the
|
|
19753
|
+
* decision vanishing. Only one replaced by a newer decision drops out.
|
|
19754
|
+
*/
|
|
19755
|
+
function onLiveRegister(item) {
|
|
19756
|
+
return isActive(item) || item.kind === "decision" && item.status === "decided";
|
|
19757
|
+
}
|
|
19758
|
+
|
|
19726
19759
|
// src/lib/csv.ts
|
|
19727
19760
|
/**
|
|
19728
19761
|
* Text a spreadsheet would run as a formula: a title from an intake form
|
|
@@ -20001,6 +20034,7 @@ function tasksCsv(data, projectId) {
|
|
|
20001
20034
|
"Blocked by",
|
|
20002
20035
|
"Description",
|
|
20003
20036
|
"Subtask of",
|
|
20037
|
+
"Starts",
|
|
20004
20038
|
...fields.map((f) => f.name)
|
|
20005
20039
|
], tasks.map((t) => [
|
|
20006
20040
|
taskKey(t, project) ?? "",
|
|
@@ -20013,6 +20047,7 @@ function tasksCsv(data, projectId) {
|
|
|
20013
20047
|
blockerIds(t).map((id) => here.get(id)).filter((b) => Boolean(b)).map(name).join("; "),
|
|
20014
20048
|
t.description,
|
|
20015
20049
|
((p) => p ? name(p) : "")(parentOf(t, data.tasks)),
|
|
20050
|
+
t.startDate ?? "",
|
|
20016
20051
|
...fields.map((f) => f.type === "url" ? safeLink(t.customFieldValues?.[f.id]) ?? "" : fieldValueText(f, t.customFieldValues?.[f.id], nameOf))
|
|
20017
20052
|
]));
|
|
20018
20053
|
}
|
|
@@ -21437,6 +21472,7 @@ const HARBOR_TOOLS = [
|
|
|
21437
21472
|
position: boardPositions(data.tasks).get(t.id) ?? null,
|
|
21438
21473
|
priority: t.priority,
|
|
21439
21474
|
due: dayOf(t.dueDate),
|
|
21475
|
+
...t.startDate ? { starts: dayOf(t.startDate) } : {},
|
|
21440
21476
|
assignee: nameOf$2(people, t.assigneeId),
|
|
21441
21477
|
...alsoOnOf(t, people),
|
|
21442
21478
|
project: project?.name ?? null,
|
|
@@ -21756,7 +21792,7 @@ const HARBOR_TOOLS = [
|
|
|
21756
21792
|
tool({
|
|
21757
21793
|
name: "list_risks",
|
|
21758
21794
|
title: "Risks, issues and decisions",
|
|
21759
|
-
description: "The RAID register, highest score first: open risks
|
|
21795
|
+
description: "The RAID register, highest score first: open risks and issues, and decisions still to make or standing, unless asked for all (closed, resolved and replaced ones too). Each item says when it was logged and last changed." + DATA_NOTE,
|
|
21760
21796
|
input: object({
|
|
21761
21797
|
harbor: harborArg,
|
|
21762
21798
|
kind: array(_enum([
|
|
@@ -21773,7 +21809,7 @@ const HARBOR_TOOLS = [
|
|
|
21773
21809
|
const { data } = ctx;
|
|
21774
21810
|
const people = byId(data.people);
|
|
21775
21811
|
const project = input.project ? findOne(data.projects, input.project, "project") : null;
|
|
21776
|
-
const list = data.raidItems.filter((r) => (input.includeClosed ||
|
|
21812
|
+
const list = data.raidItems.filter((r) => (input.includeClosed || onLiveRegister(r)) && (!input.kind?.length || input.kind.includes(r.kind)) && (!project || r.projectId === project.id)).sort((a, b) => raidScore(b) - raidScore(a));
|
|
21777
21813
|
return {
|
|
21778
21814
|
total: list.length,
|
|
21779
21815
|
items: list.slice(input.offset, input.offset + input.limit).map((r) => ({
|
|
@@ -21789,7 +21825,9 @@ const HARBOR_TOOLS = [
|
|
|
21789
21825
|
due: dayOf(r.dueDate),
|
|
21790
21826
|
...raidScope(data, r),
|
|
21791
21827
|
...r.mitigation ? { mitigation: r.mitigation } : {},
|
|
21792
|
-
...r.resolution ? { resolution: r.resolution } : {}
|
|
21828
|
+
...r.resolution ? { resolution: r.resolution } : {},
|
|
21829
|
+
createdAt: r.createdAt,
|
|
21830
|
+
updatedAt: r.updatedAt
|
|
21793
21831
|
}))
|
|
21794
21832
|
};
|
|
21795
21833
|
}
|
|
@@ -22091,6 +22129,7 @@ const HARBOR_TOOLS = [
|
|
|
22091
22129
|
const today = todayISO(ctx.now);
|
|
22092
22130
|
const people = byId(data.people);
|
|
22093
22131
|
const named = byId([...data.programs, ...data.projects]);
|
|
22132
|
+
const tasksById = byId(data.tasks);
|
|
22094
22133
|
return { portfolios: (input.portfolio ? [findOne(data.portfolios, input.portfolio, "portfolio")] : data.portfolios).map((pf) => ({
|
|
22095
22134
|
id: pf.id,
|
|
22096
22135
|
name: pf.name,
|
|
@@ -22101,6 +22140,7 @@ const HARBOR_TOOLS = [
|
|
|
22101
22140
|
id: g.id,
|
|
22102
22141
|
title: g.title,
|
|
22103
22142
|
owner: g.ownerId ? people.get(g.ownerId)?.name ?? null : null,
|
|
22143
|
+
...g.start ? { start: g.start } : {},
|
|
22104
22144
|
due: g.due,
|
|
22105
22145
|
...g.why ? { why: g.why } : {},
|
|
22106
22146
|
progress: Math.round(progress * 100),
|
|
@@ -22117,7 +22157,7 @@ const HARBOR_TOOLS = [
|
|
|
22117
22157
|
target: kr.target,
|
|
22118
22158
|
current: kr.current ?? kr.start ?? 0
|
|
22119
22159
|
} : {
|
|
22120
|
-
counts: (kr.linkIds ?? []).map((id) => named.get(id)?.name ?? id),
|
|
22160
|
+
counts: (kr.linkIds ?? []).map((id) => named.get(id)?.name ?? tasksById.get(id)?.title ?? id),
|
|
22121
22161
|
projects: linkedProjects(kr, data.projects).length
|
|
22122
22162
|
}
|
|
22123
22163
|
}))
|
|
@@ -23921,11 +23961,13 @@ function nextOccurrence(task, id, today, createdAt, siblings = []) {
|
|
|
23921
23961
|
if (!task.repeat) return null;
|
|
23922
23962
|
const dueDate = task.dueDate ? nextSeriesDate(task.dueDate, task.repeat, today) : nextDueDate(today, task.repeat);
|
|
23923
23963
|
if (siblings.some((t) => t.status !== "done" && t.repeat === task.repeat && t.title === task.title && (t.dueDate ?? "") >= dueDate)) return null;
|
|
23964
|
+
const lead = task.startDate && task.dueDate ? differenceInCalendarDays(/* @__PURE__ */ new Date(`${task.dueDate.slice(0, 10)}T12:00:00`), /* @__PURE__ */ new Date(`${task.startDate.slice(0, 10)}T12:00:00`)) : null;
|
|
23924
23965
|
return {
|
|
23925
23966
|
...task,
|
|
23926
23967
|
id,
|
|
23927
23968
|
status: "todo",
|
|
23928
23969
|
dueDate,
|
|
23970
|
+
...task.startDate ? { startDate: lead != null && lead >= 0 ? format(addDays(/* @__PURE__ */ new Date(`${dueDate}T12:00:00`), -lead), "yyyy-MM-dd") : null } : {},
|
|
23929
23971
|
createdAt,
|
|
23930
23972
|
completedAt: null,
|
|
23931
23973
|
blockedById: null,
|
|
@@ -23939,6 +23981,7 @@ function nextOccurrence(task, id, today, createdAt, siblings = []) {
|
|
|
23939
23981
|
var REPEAT_LABEL, iso, step;
|
|
23940
23982
|
var init_recurrence = __esmMin((() => {
|
|
23941
23983
|
init_date_fns();
|
|
23984
|
+
init_date_text();
|
|
23942
23985
|
REPEAT_LABEL = {
|
|
23943
23986
|
weekly: "Every week",
|
|
23944
23987
|
biweekly: "Every two weeks",
|
|
@@ -24004,6 +24047,18 @@ var init_bulk = __esmMin((() => {
|
|
|
24004
24047
|
plural$2 = (n) => `${n} ${n === 1 ? "task" : "tasks"}`;
|
|
24005
24048
|
}));
|
|
24006
24049
|
|
|
24050
|
+
// src/lib/task-start.ts
|
|
24051
|
+
function fitStart(start, due) {
|
|
24052
|
+
const s = start ? start.slice(0, 10) : "";
|
|
24053
|
+
if (!DAY.test(s)) return null;
|
|
24054
|
+
const d = due ? due.slice(0, 10) : "";
|
|
24055
|
+
return DAY.test(d) && s > d ? d : s;
|
|
24056
|
+
}
|
|
24057
|
+
var DAY;
|
|
24058
|
+
var init_task_start = __esmMin((() => {
|
|
24059
|
+
DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
24060
|
+
}));
|
|
24061
|
+
|
|
24007
24062
|
// src/lib/slices/tasks.ts
|
|
24008
24063
|
function tasksSlice({ set, get, write }) {
|
|
24009
24064
|
return {
|
|
@@ -24040,6 +24095,7 @@ function tasksSlice({ set, get, write }) {
|
|
|
24040
24095
|
completedAt: t.status === "done" ? t.completedAt ?? stamp() : null,
|
|
24041
24096
|
checklist: t.checklist ?? [],
|
|
24042
24097
|
..."parentTaskId" in t ? { parentTaskId } : {},
|
|
24098
|
+
...t.startDate ? { startDate: fitStart(t.startDate, t.dueDate) } : {},
|
|
24043
24099
|
...blockersPatch(blockerIds({
|
|
24044
24100
|
id,
|
|
24045
24101
|
blockedById: t.blockedById,
|
|
@@ -24105,6 +24161,7 @@ function tasksSlice({ set, get, write }) {
|
|
|
24105
24161
|
...patch,
|
|
24106
24162
|
touchedAt: stamp()
|
|
24107
24163
|
};
|
|
24164
|
+
if (next.startDate) next.startDate = fitStart(next.startDate, next.dueDate);
|
|
24108
24165
|
if (next.assigneeId && next.followerIds?.includes(next.assigneeId)) next.followerIds = alsoOnIds(next);
|
|
24109
24166
|
if (patch.status && patch.status !== row.status) {
|
|
24110
24167
|
if (patch.status === "done" && !patch.completedAt) next.completedAt = stamp();
|
|
@@ -24569,6 +24626,7 @@ var init_tasks = __esmMin((() => {
|
|
|
24569
24626
|
init_bulk();
|
|
24570
24627
|
init_task_people();
|
|
24571
24628
|
init_subtasks();
|
|
24629
|
+
init_task_start();
|
|
24572
24630
|
}));
|
|
24573
24631
|
|
|
24574
24632
|
// src/lib/raid-history.ts
|
package/lib/cli.mjs
CHANGED
|
@@ -165,7 +165,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
165
165
|
--from: plan it from a finished
|
|
166
166
|
project as that one really ran
|
|
167
167
|
harbor add "TITLE" [--project P] [--assignee me|none|NAME]
|
|
168
|
-
[--also "NAME, NAME"] [--due DATE|pilot]
|
|
168
|
+
[--also "NAME, NAME"] [--due DATE|pilot] [--starts DATE]
|
|
169
169
|
[--priority low|medium|high|urgent]
|
|
170
170
|
[--status S] [--comment TEXT] [--over-limit] [--dry-run]
|
|
171
171
|
[--literal] [--template NAME] [--parent TASK]
|
|
@@ -181,6 +181,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
181
181
|
or Done
|
|
182
182
|
harbor edit TASK [--title T] [--assignee me|none|NAME]
|
|
183
183
|
[--also "NAME, NAME"|none] [--due DATE|pilot|none]
|
|
184
|
+
[--starts DATE|none]
|
|
184
185
|
[--priority P] [--status S] [--notes TEXT]
|
|
185
186
|
[--repeat weekly|biweekly|monthly|never]
|
|
186
187
|
[--waits-on TASK,TASK|none] [--parent TASK|none]
|
|
@@ -705,7 +706,7 @@ export function hint(message, project) {
|
|
|
705
706
|
)
|
|
706
707
|
.replace(
|
|
707
708
|
/^Nothing to change: pass a field to update\.$/,
|
|
708
|
-
"Nothing to change. Say what with --title, --assignee, --also, --due, --priority, --status, --notes, --repeat, --waits-on or --parent.",
|
|
709
|
+
"Nothing to change. Say what with --title, --assignee, --also, --due, --starts, --priority, --status, --notes, --repeat, --waits-on or --parent.",
|
|
709
710
|
)
|
|
710
711
|
.replace(
|
|
711
712
|
/^This connection was allowed to read only\..*$/s,
|
|
@@ -790,6 +791,10 @@ const DETAIL = {
|
|
|
790
791
|
" own owner and date, shown in the parent's details rather than on the",
|
|
791
792
|
" board. It goes in the parent's project; subtasks go one level deep.",
|
|
792
793
|
"",
|
|
794
|
+
" --starts DATE gives it a start day as well as a due day, so the project's",
|
|
795
|
+
" timeline draws it as a bar from one to the other; a start after the due",
|
|
796
|
+
" day comes to the due day. harbor edit TASK --starts DATE|none moves it.",
|
|
797
|
+
"",
|
|
793
798
|
" --due pilot takes the earliest day the owner's queue suggests: their open",
|
|
794
799
|
" work due first, their weekly pace, time away, what it waits on and how",
|
|
795
800
|
" long their similar tasks took. It needs an owner (--assignee none is",
|
|
@@ -2293,6 +2298,7 @@ export async function run(argv, io = {}) {
|
|
|
2293
2298
|
),
|
|
2294
2299
|
];
|
|
2295
2300
|
if (t.alsoOn?.length) lines.push(`Also on it: ${t.alsoOn.join(", ")}.`);
|
|
2301
|
+
if (t.starts) lines.push(`Starts ${t.starts}${t.due ? `, due ${t.due}` : ""}.`);
|
|
2296
2302
|
if (t.subtaskOf) lines.push(`Subtask of ${t.subtaskOf.key ?? t.subtaskOf.id} ${t.subtaskOf.title}.`);
|
|
2297
2303
|
if (t.repeats) lines.push(`Repeats ${REPEAT_WORD[t.repeats] ?? t.repeats}.`);
|
|
2298
2304
|
for (const [name, v] of Object.entries(t.fields ?? {}))
|
|
@@ -2546,7 +2552,7 @@ export async function run(argv, io = {}) {
|
|
|
2546
2552
|
const { positionals, values, harbor } = parse(
|
|
2547
2553
|
spec(
|
|
2548
2554
|
["over-limit", "dry-run", "literal"],
|
|
2549
|
-
["project", "assignee", "also", "due", "priority", "status", "comment", "template", "parent"],
|
|
2555
|
+
["project", "assignee", "also", "due", "starts", "priority", "status", "comment", "template", "parent"],
|
|
2550
2556
|
),
|
|
2551
2557
|
);
|
|
2552
2558
|
// A subtask goes in its parent's project.
|
|
@@ -2652,6 +2658,7 @@ export async function run(argv, io = {}) {
|
|
|
2652
2658
|
also: values.also === undefined ? undefined : alsoList(values.also),
|
|
2653
2659
|
due: duePilot(values.due) ? undefined : (dueDate(values.due) ?? words.due?.value),
|
|
2654
2660
|
suggestedDue: duePilot(values.due),
|
|
2661
|
+
starts: dueDate(values.starts),
|
|
2655
2662
|
priority: values.priority ?? words.priority?.value,
|
|
2656
2663
|
status: values.status === undefined ? undefined : statusWord(values.status),
|
|
2657
2664
|
parent: values.parent,
|
|
@@ -2775,6 +2782,7 @@ export async function run(argv, io = {}) {
|
|
|
2775
2782
|
"assignee",
|
|
2776
2783
|
"also",
|
|
2777
2784
|
"due",
|
|
2785
|
+
"starts",
|
|
2778
2786
|
"priority",
|
|
2779
2787
|
"status",
|
|
2780
2788
|
"notes",
|
|
@@ -2798,6 +2806,7 @@ export async function run(argv, io = {}) {
|
|
|
2798
2806
|
? undefined
|
|
2799
2807
|
: nullable(dueDate(values.due)),
|
|
2800
2808
|
suggestedDue: duePilot(values.due),
|
|
2809
|
+
starts: values.starts === undefined ? undefined : nullable(dueDate(values.starts)),
|
|
2801
2810
|
priority: values.priority,
|
|
2802
2811
|
status: values.status === undefined ? undefined : statusWord(values.status),
|
|
2803
2812
|
description: values.notes,
|
|
@@ -2818,6 +2827,11 @@ export async function run(argv, io = {}) {
|
|
|
2818
2827
|
(r) =>
|
|
2819
2828
|
[
|
|
2820
2829
|
`Updated: ${taskLine(r.updated, opts)}`,
|
|
2830
|
+
values.starts !== undefined
|
|
2831
|
+
? r.updated.starts
|
|
2832
|
+
? `Starts ${r.updated.starts}${r.updated.due ? `, due ${r.updated.due}` : ""}.`
|
|
2833
|
+
: "No start day now."
|
|
2834
|
+
: null,
|
|
2821
2835
|
values.parent !== undefined
|
|
2822
2836
|
? r.updated.subtaskOf
|
|
2823
2837
|
? `A subtask of ${r.updated.subtaskOf}.`
|
package/lib/exit.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leaving once what was written has left. Into a pipe, stdout is written
|
|
3
|
+
* asynchronously, so `process.exit()` straight after a big write cut
|
|
4
|
+
* `harbor export harbor | jq` off at 64 KB with no error. A zero-length
|
|
5
|
+
* write's callback runs after every write before it, so waiting on one per
|
|
6
|
+
* stream is enough; the exit after it still stops anything left running.
|
|
7
|
+
*/
|
|
8
|
+
const flushed = (stream) =>
|
|
9
|
+
new Promise((resolve) => {
|
|
10
|
+
if (!stream.writableLength) resolve();
|
|
11
|
+
else stream.write("", () => resolve());
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export async function exitWhenWritten(code, streams = [process.stdout, process.stderr]) {
|
|
15
|
+
process.exitCode = code;
|
|
16
|
+
await Promise.all(streams.map(flushed));
|
|
17
|
+
process.exit(code);
|
|
18
|
+
}
|