@harborppm/cli 0.95.0 → 0.97.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/dist/app.mjs +1079 -53
- package/lib/cli.mjs +70 -0
- package/lib/format.mjs +39 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -54,6 +54,8 @@ import {
|
|
|
54
54
|
catchupText,
|
|
55
55
|
handoverText,
|
|
56
56
|
fogText,
|
|
57
|
+
scuttleText,
|
|
58
|
+
scuttleCallText,
|
|
57
59
|
agentWorkText,
|
|
58
60
|
raidDetail,
|
|
59
61
|
raidList,
|
|
@@ -295,6 +297,13 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
295
297
|
harbor fog --sweep [--project P] one at a time: still needed, a
|
|
296
298
|
new date, or drop it
|
|
297
299
|
harbor fog keep TASK it's still needed: out of the fog
|
|
300
|
+
harbor scuttle [--program P|--portfolio P] projects that look adrift: still
|
|
301
|
+
for weeks, slipping, long late
|
|
302
|
+
harbor scuttle sail|refit PROJECT [--end DATE] [--note TEXT]
|
|
303
|
+
keep it, or refit it, for three
|
|
304
|
+
months out of the line-up
|
|
305
|
+
harbor scuttle PROJECT --why "TEXT" stop it on purpose: Done, on the
|
|
306
|
+
register, its tasks set free
|
|
298
307
|
harbor watch [--project P] [--bell] a live ticker of the harbor's
|
|
299
308
|
changes; yours bright, q stops
|
|
300
309
|
harbor watch --once [--since TIME] [--json] the latest changes (or all since
|
|
@@ -1149,6 +1158,24 @@ const DETAIL = {
|
|
|
1149
1158
|
" The sweep needs a terminal and full access (harbor login without",
|
|
1150
1159
|
" --read-only), since dropping a task deletes it.",
|
|
1151
1160
|
],
|
|
1161
|
+
scuttle: [
|
|
1162
|
+
" harbor scuttle [--program P] reads: running projects that look",
|
|
1163
|
+
" [--portfolio P] adrift (nothing moved for four",
|
|
1164
|
+
" weeks, slipping again and again, long",
|
|
1165
|
+
" past the end, paused for a month),",
|
|
1166
|
+
" and what stopping each would free",
|
|
1167
|
+
" harbor scuttle sail PROJECT writes: keep it sailing; it leaves",
|
|
1168
|
+
" the line-up for three months",
|
|
1169
|
+
" harbor scuttle refit PROJECT writes: a new end date and/or a line",
|
|
1170
|
+
' [--end DATE] [--note "TEXT"] on what changes',
|
|
1171
|
+
' harbor scuttle PROJECT --why "TEXT" writes: stop it. It closes as Done',
|
|
1172
|
+
" with the why as its note, a decision",
|
|
1173
|
+
" goes on the register, and its open",
|
|
1174
|
+
" tasks come off people's lists",
|
|
1175
|
+
"",
|
|
1176
|
+
" Nothing is deleted: harbor health auto --project P raises a scuttled project",
|
|
1177
|
+
" again and gives its tasks back.",
|
|
1178
|
+
],
|
|
1152
1179
|
watch: [
|
|
1153
1180
|
" harbor watch reads, live: each change to the harbor",
|
|
1154
1181
|
" as it lands, one line with its time",
|
|
@@ -3579,6 +3606,49 @@ export async function run(argv, io = {}) {
|
|
|
3579
3606
|
}
|
|
3580
3607
|
return call("fog", { harbor, project: values.project, limit: 100 }, (r) => fogText(r));
|
|
3581
3608
|
}
|
|
3609
|
+
case "scuttle": {
|
|
3610
|
+
const { positionals, values, harbor } = parse(
|
|
3611
|
+
spec(["list"], ["program", "portfolio", "end", "note", "why"]),
|
|
3612
|
+
);
|
|
3613
|
+
const [first, ...rest] = positionals;
|
|
3614
|
+
if (first === "sail" || first === "refit") {
|
|
3615
|
+
if (values.why !== undefined)
|
|
3616
|
+
throw new HarborError(`--why goes with scuttling: harbor scuttle PROJECT --why "TEXT".`);
|
|
3617
|
+
const project = one(rest, "project");
|
|
3618
|
+
if (first === "sail" && (values.end !== undefined || values.note !== undefined))
|
|
3619
|
+
throw new HarborError("--end and --note go with a refit: harbor scuttle refit PROJECT --end DATE.");
|
|
3620
|
+
if (first === "refit" && values.end === undefined && values.note === undefined)
|
|
3621
|
+
throw new HarborError('A refit needs --end DATE or --note "what changes".');
|
|
3622
|
+
return call(
|
|
3623
|
+
"answer_scuttle_or_sail",
|
|
3624
|
+
{
|
|
3625
|
+
harbor,
|
|
3626
|
+
project,
|
|
3627
|
+
call: first,
|
|
3628
|
+
...(values.end !== undefined ? { endDate: dueDate(values.end) } : {}),
|
|
3629
|
+
...(values.note !== undefined ? { note: values.note } : {}),
|
|
3630
|
+
},
|
|
3631
|
+
scuttleCallText,
|
|
3632
|
+
);
|
|
3633
|
+
}
|
|
3634
|
+
if (first !== undefined) {
|
|
3635
|
+
const project = one(positionals, "project");
|
|
3636
|
+
if (values.why === undefined || !values.why.trim())
|
|
3637
|
+
throw new HarborError(
|
|
3638
|
+
`Say why it stops: harbor scuttle ${project} --why "TEXT". To keep it: harbor scuttle sail ${project}.`,
|
|
3639
|
+
);
|
|
3640
|
+
return call(
|
|
3641
|
+
"answer_scuttle_or_sail",
|
|
3642
|
+
{ harbor, project, call: "scuttle", why: values.why },
|
|
3643
|
+
scuttleCallText,
|
|
3644
|
+
);
|
|
3645
|
+
}
|
|
3646
|
+
return call(
|
|
3647
|
+
"scuttle_or_sail",
|
|
3648
|
+
{ harbor, program: values.program, portfolio: values.portfolio, limit: 100 },
|
|
3649
|
+
scuttleText,
|
|
3650
|
+
);
|
|
3651
|
+
}
|
|
3582
3652
|
case "bridge": {
|
|
3583
3653
|
const { positionals, values, harbor } = parse(spec(["list"], ["program"]));
|
|
3584
3654
|
none(positionals);
|
package/lib/format.mjs
CHANGED
|
@@ -1124,6 +1124,45 @@ export function fogText(r, { sweepHint = true } = {}) {
|
|
|
1124
1124
|
return lines.join("\n");
|
|
1125
1125
|
}
|
|
1126
1126
|
|
|
1127
|
+
/** harbor scuttle: projects that look adrift, with what stopping each would free. */
|
|
1128
|
+
export function scuttleText(r) {
|
|
1129
|
+
const where = r.program ? ` in ${r.program}` : r.portfolio ? ` in ${r.portfolio}` : "";
|
|
1130
|
+
if (!r.adrift)
|
|
1131
|
+
return `Nothing looks adrift${where}: every running project has moved lately, and none keeps slipping.`;
|
|
1132
|
+
const lines = [
|
|
1133
|
+
`${r.adrift} ${r.adrift === 1 ? "project looks" : "projects look"} adrift${where}. Keep each sailing, refit it, or stop it on purpose.`,
|
|
1134
|
+
"",
|
|
1135
|
+
];
|
|
1136
|
+
for (const p of r.projects) {
|
|
1137
|
+
lines.push(
|
|
1138
|
+
` ${(p.key ?? "").padEnd(5)} ${p.project}${p.owner ? ` · ${p.owner}` : ""} · ends ${p.endDate}`,
|
|
1139
|
+
);
|
|
1140
|
+
for (const s of p.signs) lines.push(` ${s.text}`);
|
|
1141
|
+
const frees = [
|
|
1142
|
+
`${p.openTasks} open task${p.openTasks === 1 ? "" : "s"}`,
|
|
1143
|
+
p.crew.length ? `with ${p.crew.map((c) => c.person).filter(Boolean).join(", ")}` : null,
|
|
1144
|
+
p.hoursPerWeek ? `${p.hoursPerWeek}h a week logged` : null,
|
|
1145
|
+
p.spendPerWeek ? `$${p.spendPerWeek.toLocaleString("en-US")} a week of spend` : null,
|
|
1146
|
+
];
|
|
1147
|
+
lines.push(` holds ${frees.filter(Boolean).join(" · ")}`);
|
|
1148
|
+
}
|
|
1149
|
+
if (r.more) lines.push("", `And ${r.adrift - r.projects.length} more.`);
|
|
1150
|
+
lines.push(
|
|
1151
|
+
"",
|
|
1152
|
+
'harbor scuttle sail PROJECT keeps one, harbor scuttle refit PROJECT --end DATE refits it, harbor scuttle PROJECT --why "TEXT" stops it.',
|
|
1153
|
+
);
|
|
1154
|
+
return lines.join("\n");
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/** What harbor scuttle sail, refit or PROJECT --why did. */
|
|
1158
|
+
export function scuttleCallText(r) {
|
|
1159
|
+
if (r.call === "scuttle")
|
|
1160
|
+
return `Scuttled ${r.project}. ${r.sendOff} The why is on the register as a decision; harbor health auto --project "${r.project}" raises it again.`;
|
|
1161
|
+
if (r.call === "refit")
|
|
1162
|
+
return `Refitted ${r.project}: ${r.note ?? `ends ${r.endDate}`}. It's out of the line-up until ${r.quietUntil}.`;
|
|
1163
|
+
return `${r.project} sails on. It's out of the line-up until ${r.quietUntil}.`;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1127
1166
|
/** harbor lifeboats: work only one person knows, numbered for harbor lifeboats N. */
|
|
1128
1167
|
export function lifeboatsText(r) {
|
|
1129
1168
|
const where = r.program ? ` in ${r.program}` : "";
|