@harborppm/cli 0.98.0 → 0.99.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/lib/format.mjs CHANGED
@@ -30,13 +30,20 @@ export function statusWord(typed) {
30
30
  * terminal's, when printing to one) shortens a long title so the line fits;
31
31
  * piped output keeps every title whole.
32
32
  */
33
+ /**
34
+ * A task's status in words: its board's own name for the column when the
35
+ * project renamed it ("in progress"), else Harbor's ("in play").
36
+ */
37
+ export const statusText = (t) =>
38
+ t.column ? t.column.toLowerCase() : (STATUS[t.status] ?? t.status);
39
+
33
40
  export function taskLine(t, { today, mine = false, width } = {}) {
34
41
  const open = t.status !== "done";
35
42
  const late = t.due && today && t.due < today && open;
36
43
  // Today and tomorrow in words, so what's close stands out from the dates.
37
44
  const soon = open && today && t.due ? soonWord(t.due, today) : null;
38
45
  const bits = [
39
- STATUS[t.status] ?? t.status,
46
+ statusText(t),
40
47
  t.due ? (late ? `late, due ${t.due}` : soon ? `due ${soon}` : `due ${t.due}`) : null,
41
48
  t.priority && t.priority !== "medium" ? t.priority : null,
42
49
  mine ? null : t.assignee ? `${t.assignee}${t.alsoOn?.length ? ` +${t.alsoOn.length}` : ""}` : null,
@@ -115,6 +122,12 @@ const hiddenNote = (n, what) =>
115
122
  * Each portfolio's programs and their projects, indented, as harbor_overview
116
123
  * gives them. Programs and projects set to Done stay out unless `done`.
117
124
  */
125
+ /** "starts 2026-11-02 · ends 2026-12-18", or whichever of the two it has. */
126
+ const datesOf = (x) =>
127
+ [x.starts ? `starts ${x.starts}` : null, x.ends ? `ends ${x.ends}` : null]
128
+ .filter(Boolean)
129
+ .join(" · ") || null;
130
+
118
131
  export function programTree(r, { done = false } = {}) {
119
132
  const portfolios = r.portfolios ?? [];
120
133
  if (!portfolios.some((pf) => pf.programs.length)) {
@@ -129,7 +142,7 @@ export function programTree(r, { done = false } = {}) {
129
142
  if (portfolios.length > 1 && programs.length) out.push(pf.name);
130
143
  const pad = portfolios.length > 1 ? " " : "";
131
144
  for (const pg of programs) {
132
- const bits = [health(pg.health), pg.ends ? `ends ${pg.ends}` : null].filter(Boolean);
145
+ const bits = [health(pg.health), datesOf(pg)].filter(Boolean);
133
146
  out.push(`${pad}${pg.name} (${bits.join(" · ")})`);
134
147
  const projects = pg.projects.filter(shown);
135
148
  hidden += pg.projects.length - projects.length;
@@ -138,7 +151,7 @@ export function programTree(r, { done = false } = {}) {
138
151
  health(pj.health),
139
152
  `${pj.openTasks} open`,
140
153
  pj.lateTasks ? `${pj.lateTasks} late` : null,
141
- pj.ends ? `ends ${pj.ends}` : null,
154
+ datesOf(pj),
142
155
  ].filter(Boolean);
143
156
  out.push(`${pad} ${pj.name} (${work.join(" · ")})`);
144
157
  }
@@ -1006,7 +1019,7 @@ export function agentWorkText(r) {
1006
1019
  const lines = [`Work for ${r.agent}${r.addedBy ? ` (added by ${r.addedBy})` : ""}`];
1007
1020
  if (!r.tasks?.length) lines.push("", "Nothing to do: no open tasks are assigned to it.");
1008
1021
  for (const t of r.tasks ?? []) {
1009
- const bits = [STATUS[t.status] ?? t.status, t.priority];
1022
+ const bits = [statusText(t), t.priority];
1010
1023
  if (t.due) bits.push(`due ${t.due}`);
1011
1024
  lines.push(
1012
1025
  "",
@@ -1381,3 +1394,83 @@ export function messagesText(r) {
1381
1394
  ),
1382
1395
  ].join("\n\n");
1383
1396
  }
1397
+
1398
+ const SIGN_OFF = {
1399
+ pending: "waiting on the sign-off",
1400
+ approved: "signed off",
1401
+ changes_requested: "changes asked for",
1402
+ };
1403
+
1404
+ /** One milestone's line: due, reached or late, its tasks, sign-off and waits. */
1405
+ export function milestoneLine(m, { program = true } = {}) {
1406
+ const bits = [
1407
+ m.reached ? "reached" : m.late ? `late, due ${m.due}` : `due ${m.due}`,
1408
+ m.tasks
1409
+ ? `${m.tasks.done} of ${m.tasks.total} task${m.tasks.total === 1 ? "" : "s"} done`
1410
+ : null,
1411
+ m.signOff
1412
+ ? `${m.signOff.by ?? "someone"} signs off: ${SIGN_OFF[m.signOff.status] ?? m.signOff.status}${m.signOff.note ? ` (“${m.signOff.note}”)` : ""}`
1413
+ : null,
1414
+ m.waitsOn?.length
1415
+ ? `waits on ${m.waitsOn.map((w) => `${w.project}${w.done ? " (done)" : ""}`).join(", ")}`
1416
+ : null,
1417
+ program && m.program ? m.program : null,
1418
+ ].filter(Boolean);
1419
+ return `${m.name} (${bits.join(" · ")})`;
1420
+ }
1421
+
1422
+ /** list_milestones: soonest first, the program named once when it's one. */
1423
+ export function milestonesText(r) {
1424
+ const list = r.milestones ?? [];
1425
+ const hidden = r.reachedHidden
1426
+ ? `${r.reachedHidden} reached ${r.reachedHidden === 1 ? "one" : "ones"} not shown (--done shows them).`
1427
+ : "";
1428
+ if (!list.length) {
1429
+ const none = r.program ? `${r.program} has no open milestones.` : "No open milestones.";
1430
+ return [none, hidden || `Add one: harbor milestone "NAME" --new --program P --due DATE.`]
1431
+ .filter(Boolean)
1432
+ .join(" ");
1433
+ }
1434
+ return [
1435
+ ...(r.program ? [`${r.program}:`] : []),
1436
+ ...list.map((m) => `${r.program ? " " : ""}${milestoneLine(m, { program: !r.program })}`),
1437
+ ...(hidden ? [hidden] : []),
1438
+ ].join("\n");
1439
+ }
1440
+
1441
+ /** One goal in full, as list_goals gives it. */
1442
+ export function goalText(g, portfolio) {
1443
+ const bits = [`${g.progress}%`, g.statusText.toLowerCase()];
1444
+ if (g.owner) bits.push(g.owner);
1445
+ if (g.due && g.status !== "achieved") bits.push(`by ${g.due}`);
1446
+ return [
1447
+ `${g.title} (${bits.join(" · ")})`,
1448
+ ...(portfolio ? [`In ${portfolio}.`] : []),
1449
+ ...(g.why ? [`Why: ${g.why}`] : []),
1450
+ ...(g.keyResults.length
1451
+ ? g.keyResults.map(
1452
+ (k) =>
1453
+ ` ${k.title} · ${k.text}${k.kind === "work" && k.counts?.length ? ` (counts ${k.counts.join(", ")})` : ""}`,
1454
+ )
1455
+ : [` No key results yet. harbor goal "${g.title}" --kr "TITLE" --target N adds one.`]),
1456
+ ].join("\n");
1457
+ }
1458
+
1459
+ /** list_dashboards: each dashboard, each chart's one-line text and its numbers. */
1460
+ export function reportsText(r) {
1461
+ const list = r.dashboards ?? [];
1462
+ if (!list.length) return "No dashboards yet. Make one on the website's Reports page.";
1463
+ const out = [];
1464
+ for (const d of list) {
1465
+ if (out.length) out.push("");
1466
+ out.push(d.name);
1467
+ if (!d.charts.length) out.push(" No charts on it yet.");
1468
+ for (const c of d.charts) {
1469
+ out.push(
1470
+ ` ${c.text}`,
1471
+ ` ${c.counts} by ${c.splitBy.toLowerCase()}${c.from ? `, in ${c.from}` : ""}`,
1472
+ );
1473
+ }
1474
+ }
1475
+ return out.join("\n");
1476
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.98.0",
3
+ "version": "0.99.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",