@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 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] no TITLE: one title a line from stdin
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
- process.exit(code);
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
- process.exit(1);
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
- let done = 0;
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
- /** How much of the goal's time has gone by, 0 to 1, from when it was set to its due day. */
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.createdAt.slice(0, 10)}T00:00:00Z`);
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
- let done = 0;
14049
- let all = 0;
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, issues and pending decisions unless asked for all." + DATA_NOTE,
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 || isActive(r)) && (!input.kind?.length || input.kind.includes(r.kind)) && (!project || r.projectId === project.id)).sort((a, b) => raidScore(b) - raidScore(a));
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.89.0",
3
+ "version": "0.91.0",
4
4
  "description": "Harbor from the terminal: a full-screen app with everything, plus commands for scripts and agents.",
5
5
  "keywords": [
6
6
  "harbor",