@harborppm/cli 0.90.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/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);
@@ -20013,6 +20034,7 @@ function tasksCsv(data, projectId) {
20013
20034
  "Blocked by",
20014
20035
  "Description",
20015
20036
  "Subtask of",
20037
+ "Starts",
20016
20038
  ...fields.map((f) => f.name)
20017
20039
  ], tasks.map((t) => [
20018
20040
  taskKey(t, project) ?? "",
@@ -20025,6 +20047,7 @@ function tasksCsv(data, projectId) {
20025
20047
  blockerIds(t).map((id) => here.get(id)).filter((b) => Boolean(b)).map(name).join("; "),
20026
20048
  t.description,
20027
20049
  ((p) => p ? name(p) : "")(parentOf(t, data.tasks)),
20050
+ t.startDate ?? "",
20028
20051
  ...fields.map((f) => f.type === "url" ? safeLink(t.customFieldValues?.[f.id]) ?? "" : fieldValueText(f, t.customFieldValues?.[f.id], nameOf))
20029
20052
  ]));
20030
20053
  }
@@ -21449,6 +21472,7 @@ const HARBOR_TOOLS = [
21449
21472
  position: boardPositions(data.tasks).get(t.id) ?? null,
21450
21473
  priority: t.priority,
21451
21474
  due: dayOf(t.dueDate),
21475
+ ...t.startDate ? { starts: dayOf(t.startDate) } : {},
21452
21476
  assignee: nameOf$2(people, t.assigneeId),
21453
21477
  ...alsoOnOf(t, people),
21454
21478
  project: project?.name ?? null,
@@ -22105,6 +22129,7 @@ const HARBOR_TOOLS = [
22105
22129
  const today = todayISO(ctx.now);
22106
22130
  const people = byId(data.people);
22107
22131
  const named = byId([...data.programs, ...data.projects]);
22132
+ const tasksById = byId(data.tasks);
22108
22133
  return { portfolios: (input.portfolio ? [findOne(data.portfolios, input.portfolio, "portfolio")] : data.portfolios).map((pf) => ({
22109
22134
  id: pf.id,
22110
22135
  name: pf.name,
@@ -22115,6 +22140,7 @@ const HARBOR_TOOLS = [
22115
22140
  id: g.id,
22116
22141
  title: g.title,
22117
22142
  owner: g.ownerId ? people.get(g.ownerId)?.name ?? null : null,
22143
+ ...g.start ? { start: g.start } : {},
22118
22144
  due: g.due,
22119
22145
  ...g.why ? { why: g.why } : {},
22120
22146
  progress: Math.round(progress * 100),
@@ -22131,7 +22157,7 @@ const HARBOR_TOOLS = [
22131
22157
  target: kr.target,
22132
22158
  current: kr.current ?? kr.start ?? 0
22133
22159
  } : {
22134
- 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),
22135
22161
  projects: linkedProjects(kr, data.projects).length
22136
22162
  }
22137
22163
  }))
@@ -23935,11 +23961,13 @@ function nextOccurrence(task, id, today, createdAt, siblings = []) {
23935
23961
  if (!task.repeat) return null;
23936
23962
  const dueDate = task.dueDate ? nextSeriesDate(task.dueDate, task.repeat, today) : nextDueDate(today, task.repeat);
23937
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;
23938
23965
  return {
23939
23966
  ...task,
23940
23967
  id,
23941
23968
  status: "todo",
23942
23969
  dueDate,
23970
+ ...task.startDate ? { startDate: lead != null && lead >= 0 ? format(addDays(/* @__PURE__ */ new Date(`${dueDate}T12:00:00`), -lead), "yyyy-MM-dd") : null } : {},
23943
23971
  createdAt,
23944
23972
  completedAt: null,
23945
23973
  blockedById: null,
@@ -23953,6 +23981,7 @@ function nextOccurrence(task, id, today, createdAt, siblings = []) {
23953
23981
  var REPEAT_LABEL, iso, step;
23954
23982
  var init_recurrence = __esmMin((() => {
23955
23983
  init_date_fns();
23984
+ init_date_text();
23956
23985
  REPEAT_LABEL = {
23957
23986
  weekly: "Every week",
23958
23987
  biweekly: "Every two weeks",
@@ -24018,6 +24047,18 @@ var init_bulk = __esmMin((() => {
24018
24047
  plural$2 = (n) => `${n} ${n === 1 ? "task" : "tasks"}`;
24019
24048
  }));
24020
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
+
24021
24062
  // src/lib/slices/tasks.ts
24022
24063
  function tasksSlice({ set, get, write }) {
24023
24064
  return {
@@ -24054,6 +24095,7 @@ function tasksSlice({ set, get, write }) {
24054
24095
  completedAt: t.status === "done" ? t.completedAt ?? stamp() : null,
24055
24096
  checklist: t.checklist ?? [],
24056
24097
  ..."parentTaskId" in t ? { parentTaskId } : {},
24098
+ ...t.startDate ? { startDate: fitStart(t.startDate, t.dueDate) } : {},
24057
24099
  ...blockersPatch(blockerIds({
24058
24100
  id,
24059
24101
  blockedById: t.blockedById,
@@ -24119,6 +24161,7 @@ function tasksSlice({ set, get, write }) {
24119
24161
  ...patch,
24120
24162
  touchedAt: stamp()
24121
24163
  };
24164
+ if (next.startDate) next.startDate = fitStart(next.startDate, next.dueDate);
24122
24165
  if (next.assigneeId && next.followerIds?.includes(next.assigneeId)) next.followerIds = alsoOnIds(next);
24123
24166
  if (patch.status && patch.status !== row.status) {
24124
24167
  if (patch.status === "done" && !patch.completedAt) next.completedAt = stamp();
@@ -24583,6 +24626,7 @@ var init_tasks = __esmMin((() => {
24583
24626
  init_bulk();
24584
24627
  init_task_people();
24585
24628
  init_subtasks();
24629
+ init_task_start();
24586
24630
  }));
24587
24631
 
24588
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.90.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",