@harborppm/cli 0.21.0 → 0.24.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 +7 -2
- package/dist/app.mjs +2907 -1677
- package/lib/cli.mjs +15 -0
- package/lib/format.mjs +49 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
writeSettings,
|
|
18
18
|
} from "./config.mjs";
|
|
19
19
|
import {
|
|
20
|
+
forecastList,
|
|
21
|
+
forecastOne,
|
|
20
22
|
STATUS,
|
|
21
23
|
harbors,
|
|
22
24
|
limitsText,
|
|
@@ -61,6 +63,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
61
63
|
harbor projects [words] [--program P] [--done]
|
|
62
64
|
projects with health and open work
|
|
63
65
|
(--json adds each board's limits)
|
|
66
|
+
harbor forecast [PROJECT] [--program P] when projects will likely finish
|
|
64
67
|
harbor mine what's on you, late first
|
|
65
68
|
harbor tasks [words] [--project P] [--assignee me|none|NAME]
|
|
66
69
|
[--late] [--status S,S]
|
|
@@ -97,6 +100,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
97
100
|
harbor risks [words] [--kind risk,issue,decision] [--project P] [--closed]
|
|
98
101
|
the register, worst first
|
|
99
102
|
(--closed adds closed ones)
|
|
103
|
+
harbor brief [--portfolio P] the weekly brief, ready to send
|
|
100
104
|
harbor premortem PROJECT ["WHAT WENT WRONG"] [--start]
|
|
101
105
|
a project's pre-mortem: start,
|
|
102
106
|
answer or read it
|
|
@@ -824,6 +828,12 @@ export async function run(argv, io = {}) {
|
|
|
824
828
|
`Opened the project ${r.created.name} in ${r.created.program} (${r.created.starts} to ${r.created.ends}, lead ${r.created.lead ?? "nobody"}). Add work with harbor add "TITLE" --project "${r.created.name}".`,
|
|
825
829
|
);
|
|
826
830
|
}
|
|
831
|
+
case "forecast": {
|
|
832
|
+
const { positionals, values, harbor } = parse(spec([], ["program"]));
|
|
833
|
+
const project = positionals.join(" ").trim();
|
|
834
|
+
if (project) return call("get_project", { harbor, project }, forecastOne);
|
|
835
|
+
return call("list_projects", { harbor, program: values.program, limit: 100 }, forecastList);
|
|
836
|
+
}
|
|
827
837
|
case "mine": {
|
|
828
838
|
const { positionals, harbor } = parse(spec());
|
|
829
839
|
none(positionals);
|
|
@@ -1107,6 +1117,11 @@ export async function run(argv, io = {}) {
|
|
|
1107
1117
|
(r) => raidList(r, positionals.join(" ")),
|
|
1108
1118
|
);
|
|
1109
1119
|
}
|
|
1120
|
+
case "brief": {
|
|
1121
|
+
const { positionals, values, harbor } = parse(spec([], ["portfolio"]));
|
|
1122
|
+
none(positionals);
|
|
1123
|
+
return call("weekly_brief", { harbor, portfolio: values.portfolio }, (r) => r.text);
|
|
1124
|
+
}
|
|
1110
1125
|
case "premortem": {
|
|
1111
1126
|
const { positionals, values, harbor } = parse(spec(["start"]));
|
|
1112
1127
|
const [project, ...words] = positionals;
|
package/lib/format.mjs
CHANGED
|
@@ -199,6 +199,55 @@ export function raidList(r, words = "") {
|
|
|
199
199
|
return lines.join("\n") + more;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/** "over 95%", in steps of five, as the website says it (src/lib/tide.ts chanceText). */
|
|
203
|
+
function chance(c) {
|
|
204
|
+
if (c < 0.05) return "under 5%";
|
|
205
|
+
if (c > 0.95) return "over 95%";
|
|
206
|
+
return `${Math.round(c * 20) * 5}%`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function finishRange(f) {
|
|
210
|
+
if (!f.likely) return "not in the next two years at this pace";
|
|
211
|
+
return `probably ${f.likely}${f.veryLikely ? `, very likely by ${f.veryLikely}` : ""}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** When each open project will likely finish at its crew's recent pace. */
|
|
215
|
+
export function forecastList(r) {
|
|
216
|
+
const projects = (r.projects ?? []).filter((p) => p.health !== "done");
|
|
217
|
+
if (!projects.length) return "No open projects match.";
|
|
218
|
+
const width = Math.min(32, Math.max(...projects.map((p) => p.name.length)));
|
|
219
|
+
return projects
|
|
220
|
+
.map((p) => {
|
|
221
|
+
const f = p.finish ?? {};
|
|
222
|
+
const name = p.name.length > width ? `${p.name.slice(0, width - 1)}…` : p.name.padEnd(width);
|
|
223
|
+
if (f.none) return `${name} no forecast: ${f.none.replace(/^\w/, (c) => c.toLowerCase())}`;
|
|
224
|
+
const end = p.ends ? ` · ${chance(f.chanceByEnd)} by its end, ${p.ends}` : "";
|
|
225
|
+
return `${name} ${finishRange(f)}${end}${f.rough ? " · rough" : ""}`;
|
|
226
|
+
})
|
|
227
|
+
.join("\n");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** One project's forecast, in a few sentences. */
|
|
231
|
+
export function forecastOne(p) {
|
|
232
|
+
const f = p.finish ?? {};
|
|
233
|
+
if (f.none) return `${p.name}: ${f.none}`;
|
|
234
|
+
const range = finishRange(f);
|
|
235
|
+
const lines = [
|
|
236
|
+
f.likely
|
|
237
|
+
? `${p.name} will ${range.replace(/^probably/, "probably finish")}${f.almostCertain ? `, almost certainly by ${f.almostCertain}` : ""}.`
|
|
238
|
+
: `${p.name} won't finish in the next two years at this pace.`,
|
|
239
|
+
];
|
|
240
|
+
if (p.ends) {
|
|
241
|
+
const c = chance(f.chanceByEnd);
|
|
242
|
+
lines.push(`${c[0].toUpperCase()}${c.slice(1)} chance of making its end date, ${p.ends}.`);
|
|
243
|
+
}
|
|
244
|
+
lines.push(
|
|
245
|
+
`${f.openTasks} task${f.openTasks === 1 ? "" : "s"} open; the crew finishes about ${f.finishedPerWeek} a week (${f.basedOn}).`,
|
|
246
|
+
);
|
|
247
|
+
if (f.rough) lines.push("Only a few tasks have finished, so treat this as rough.");
|
|
248
|
+
return lines.join("\n");
|
|
249
|
+
}
|
|
250
|
+
|
|
202
251
|
/** A project's pre-mortem, as get_premortem answers it. */
|
|
203
252
|
export function premortemText(r) {
|
|
204
253
|
if (!r.running) {
|