@harborppm/cli 0.97.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 +387 -62
- package/lib/cli.mjs +581 -85
- package/lib/format.mjs +125 -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
|
}
|
|
@@ -815,6 +828,34 @@ export function charterText(r) {
|
|
|
815
828
|
return lines.join("\n");
|
|
816
829
|
}
|
|
817
830
|
|
|
831
|
+
const LANDFALL_RESULT = { yes: "Paid off", partly: "Partly", no: "Didn't" };
|
|
832
|
+
|
|
833
|
+
/** A project's landfall as get_project, set_landfall or answer_landfall gives it. */
|
|
834
|
+
export function landfallText(project, lf) {
|
|
835
|
+
if (!lf) {
|
|
836
|
+
return `${project} has no landfall yet. Say what should be true some days after it's Done:\n harbor landfall "${project}" --expect "…" --after 90 [--measure "weekly sign-ups"]`;
|
|
837
|
+
}
|
|
838
|
+
const lines = [`Landfall · ${project}`, ` Looks like ${lf.expect}`];
|
|
839
|
+
if (lf.measure) lines.push(` Counting ${lf.measure}`);
|
|
840
|
+
if (lf.result) {
|
|
841
|
+
const value =
|
|
842
|
+
lf.value != null ? ` · ${Number(lf.value).toLocaleString("en-US")}${lf.measure ? ` ${lf.measure}` : ""}` : "";
|
|
843
|
+
lines.push(` Answer ${LANDFALL_RESULT[lf.result] ?? lf.result}${value}`);
|
|
844
|
+
if (lf.note) lines.push(` ${lf.note}`);
|
|
845
|
+
if (lf.answeredOn)
|
|
846
|
+
lines.push(` said by ${lf.answeredBy ?? "someone"} on ${lf.answeredOn}`);
|
|
847
|
+
} else if (lf.state === "due") {
|
|
848
|
+
lines.push(
|
|
849
|
+
` Due now its day was ${lf.checkOn}. Answer: harbor landfall "${project}" --answer yes|partly|no [--value N]`,
|
|
850
|
+
);
|
|
851
|
+
} else if (lf.state === "waiting") {
|
|
852
|
+
lines.push(` Check on ${lf.checkOn} (${lf.afterDays} days after Done)`);
|
|
853
|
+
} else {
|
|
854
|
+
lines.push(` Check ${lf.afterDays} days after the project is marked Done`);
|
|
855
|
+
}
|
|
856
|
+
return lines.join("\n");
|
|
857
|
+
}
|
|
858
|
+
|
|
818
859
|
export function notesText(project, notes) {
|
|
819
860
|
if (!notes?.length)
|
|
820
861
|
return `No notes for later on ${project}. Leave one: harbor note "${project}" "NOTE" --opens +30`;
|
|
@@ -978,7 +1019,7 @@ export function agentWorkText(r) {
|
|
|
978
1019
|
const lines = [`Work for ${r.agent}${r.addedBy ? ` (added by ${r.addedBy})` : ""}`];
|
|
979
1020
|
if (!r.tasks?.length) lines.push("", "Nothing to do: no open tasks are assigned to it.");
|
|
980
1021
|
for (const t of r.tasks ?? []) {
|
|
981
|
-
const bits = [
|
|
1022
|
+
const bits = [statusText(t), t.priority];
|
|
982
1023
|
if (t.due) bits.push(`due ${t.due}`);
|
|
983
1024
|
lines.push(
|
|
984
1025
|
"",
|
|
@@ -1353,3 +1394,83 @@ export function messagesText(r) {
|
|
|
1353
1394
|
),
|
|
1354
1395
|
].join("\n\n");
|
|
1355
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
|
+
}
|