@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/README.md +12 -4
- package/dist/app.mjs +196 -50
- package/lib/cli.mjs +493 -85
- package/lib/format.mjs +97 -4
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
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 = [
|
|
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
|
+
}
|