@harborppm/cli 0.39.0 → 0.41.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 +4 -3
- package/dist/app.mjs +10 -8
- package/lib/cli.mjs +65 -26
- package/lib/format.mjs +32 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -115,6 +115,7 @@ Task titles, names and notes can come from an intake form, a GitHub pull request
|
|
|
115
115
|
|
|
116
116
|
```
|
|
117
117
|
harbor harbors the harbors you belong to
|
|
118
|
+
harbor whoami who you're signed in as, and where
|
|
118
119
|
harbor use HARBOR|none the harbor to work in when you're in several
|
|
119
120
|
harbor programs programs and their projects
|
|
120
121
|
harbor projects [words] [--program P] projects with health and open work
|
|
@@ -124,7 +125,7 @@ harbor hill [--program P] where each project is on its hill
|
|
|
124
125
|
harbor hill PROJECT --at 0-100|top|done move it: up to 50 is figuring it out,
|
|
125
126
|
past 50 is getting it done
|
|
126
127
|
harbor mine what's on you, late first
|
|
127
|
-
harbor tasks [words] [--project P] [--assignee me|none|NAME] [--late] [--status S,S]
|
|
128
|
+
harbor tasks [words] [--project P] [--assignee me|none|NAME] [--late] [--status S,S] [--all]
|
|
128
129
|
harbor show TASK one task in full, with its web link
|
|
129
130
|
harbor open [TASK|PROJECT] open it on the website (or Today)
|
|
130
131
|
harbor invite EMAIL [--viewer] invite someone to your harbor by email
|
|
@@ -150,7 +151,7 @@ harbor issue "TITLE" --project P|--program P [--impact 1-5] [--owner NAME]
|
|
|
150
151
|
[--due DATE]
|
|
151
152
|
harbor decision "TITLE" --project P|--program P [--owner NAME] [--due DATE]
|
|
152
153
|
harbor risks [words] [--kind risk,issue,decision] [--project P] [--closed]
|
|
153
|
-
harbor brief [--portfolio P] [--say]
|
|
154
|
+
harbor brief [--portfolio P] [--all] [--say] the weekly brief (--all: every late and done task)
|
|
154
155
|
harbor report PROGRAM a program's status report, as Markdown
|
|
155
156
|
harbor export tasks PROJECT|risks|review [--all]
|
|
156
157
|
a spreadsheet (CSV) to stdout
|
|
@@ -177,7 +178,7 @@ Agents on the crew: put an AI app on People with Add an agent and assign it task
|
|
|
177
178
|
|
|
178
179
|
`harbor premortem` runs a pre-mortem with your crew: `--start` asks everyone "it's after the end date and this project failed: what happened?", each person adds answers alone for five minutes, and then the answers show together on the project's Risks & issues tab, where each becomes a risk in one click. On its own it prints the question and the answers you can see. Once a project is Done, `harbor voyage` prints its voyage log (planned against actual dates, the biggest slips, the risks that came true, who closed the most, and the crew's lines) and adds yours. `harbor note` leaves a note for later on a project: it stays sealed (the crew sees only that it's there and who left it) until its day, until one of the program's milestones is reached, or until the project turns Off track, and then it opens at the top of the project and in the brief.
|
|
179
180
|
|
|
180
|
-
`harbor brief --say` turns the brief into the BBC's Shipping Forecast: each program an area, wind for late work, visibility for health, gale warnings for anything off track. It prints the forecast and speaks it with your computer's own voice (`say` on a Mac, Windows' speech, or `espeak-ng` on Linux).
|
|
181
|
+
`harbor brief --say` turns the brief into the BBC's Shipping Forecast: each program an area, wind for late work, visibility for health, gale warnings for anything off track. It prints the forecast and speaks it with your computer's own voice (`say` on a Mac, Windows' speech, or `espeak-ng` on Linux, which you may need to install). With `--json` it prints the forecast and stays quiet.
|
|
181
182
|
|
|
182
183
|
Every command takes `--json` for the raw result (a refusal then comes out as `{"error": "…"}` on standard output, still with exit code 1), and `harbor COMMAND --help` shows just that command. Statuses are backlog, todo, doing, review and done. A `DATE` is YYYY-MM-DD, `today`, `tomorrow`, or `+N` for N days from today.
|
|
183
184
|
|
package/dist/app.mjs
CHANGED
|
@@ -5013,7 +5013,7 @@ function saidThisWeek(data, now, inScope) {
|
|
|
5013
5013
|
}] : [];
|
|
5014
5014
|
});
|
|
5015
5015
|
}
|
|
5016
|
-
/** How many late tasks the brief names before "and N more". */
|
|
5016
|
+
/** How many late (or finished) tasks the brief names before "and N more". */
|
|
5017
5017
|
const BRIEF_LATE_SHOWN = 8;
|
|
5018
5018
|
function byEndDate(a, b) {
|
|
5019
5019
|
return a.endDate.localeCompare(b.endDate);
|
|
@@ -5065,6 +5065,7 @@ function lateOwnersOf(data, tasks, now) {
|
|
|
5065
5065
|
}
|
|
5066
5066
|
function composeBriefBody(opts) {
|
|
5067
5067
|
const date = format(opts.date, "MMMM d, yyyy");
|
|
5068
|
+
const shown = opts.all ? Infinity : 8;
|
|
5068
5069
|
const watch = opts.watch ?? [];
|
|
5069
5070
|
const milestones = opts.milestones ?? [];
|
|
5070
5071
|
const lateMilestones = milestones.filter((m) => m.daysLate > 0);
|
|
@@ -5081,8 +5082,8 @@ function composeBriefBody(opts) {
|
|
|
5081
5082
|
const done = () => {
|
|
5082
5083
|
if (!opts.shipped?.length) return;
|
|
5083
5084
|
lines.push("Done this week");
|
|
5084
|
-
for (const t of opts.shipped.slice(0,
|
|
5085
|
-
if (opts.shipped.length >
|
|
5085
|
+
for (const t of opts.shipped.slice(0, shown)) lines.push(`• ${t.title}${t.project ? ` (${t.project})` : ""}`);
|
|
5086
|
+
if (opts.shipped.length > shown) lines.push(`• and ${opts.shipped.length - shown} more`);
|
|
5086
5087
|
lines.push("");
|
|
5087
5088
|
};
|
|
5088
5089
|
const said = () => {
|
|
@@ -5128,12 +5129,12 @@ function composeBriefBody(opts) {
|
|
|
5128
5129
|
}
|
|
5129
5130
|
if (opts.late.length) {
|
|
5130
5131
|
lines.push("Late");
|
|
5131
|
-
for (const row of opts.late.slice(0,
|
|
5132
|
+
for (const row of opts.late.slice(0, shown)) {
|
|
5132
5133
|
const when = `${row.days} day${row.days === 1 ? "" : "s"} late`;
|
|
5133
5134
|
const held = row.holdsUp?.length ? ` (holds up ${nameList(row.holdsUp.map((t) => `“${t}”`))})` : "";
|
|
5134
5135
|
lines.push(`• ${row.title} — ${row.owner ? `${row.owner}, ` : ""}${when}${held}`);
|
|
5135
5136
|
}
|
|
5136
|
-
if (opts.late.length >
|
|
5137
|
+
if (opts.late.length > shown) lines.push(`• and ${opts.late.length - shown} more`);
|
|
5137
5138
|
lines.push("");
|
|
5138
5139
|
}
|
|
5139
5140
|
done();
|
|
@@ -5334,9 +5335,10 @@ function briefOf(data, opts = {}) {
|
|
|
5334
5335
|
quiet: grouped.offTrack.length === 0 && grouped.atRisk.length === 0 && lateOwners.length === 0 && watch.length === 0 && milestonesLate.length === 0
|
|
5335
5336
|
};
|
|
5336
5337
|
}
|
|
5337
|
-
function composeBriefBodyFrom(brief, data, workspace) {
|
|
5338
|
+
function composeBriefBodyFrom(brief, data, workspace, all = false) {
|
|
5338
5339
|
return composeBriefBody({
|
|
5339
5340
|
workspace,
|
|
5341
|
+
all,
|
|
5340
5342
|
date: brief.asOf,
|
|
5341
5343
|
offTrack: brief.offTrackPrograms,
|
|
5342
5344
|
atRisk: brief.atRiskPrograms,
|
|
@@ -5375,8 +5377,8 @@ function composeBriefBodyFrom(brief, data, workspace) {
|
|
|
5375
5377
|
}))
|
|
5376
5378
|
});
|
|
5377
5379
|
}
|
|
5378
|
-
function renderBriefText(brief, data, workspace = "Harbor") {
|
|
5379
|
-
return composeBriefBodyFrom(brief, data, workspace);
|
|
5380
|
+
function renderBriefText(brief, data, workspace = "Harbor", all = false) {
|
|
5381
|
+
return composeBriefBodyFrom(brief, data, workspace, all);
|
|
5380
5382
|
}
|
|
5381
5383
|
|
|
5382
5384
|
//#endregion
|
package/lib/cli.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
forecastOne,
|
|
24
24
|
STATUS,
|
|
25
25
|
harbors,
|
|
26
|
+
whoami,
|
|
26
27
|
limitsText,
|
|
27
28
|
hillData,
|
|
28
29
|
hillList,
|
|
@@ -62,6 +63,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
62
63
|
harbor login [--server URL] [--read-only] sign in (opens your browser once)
|
|
63
64
|
harbor logout forget the sign-in on this machine
|
|
64
65
|
harbor harbors the harbors you belong to
|
|
66
|
+
harbor whoami who you're signed in as, and where
|
|
65
67
|
harbor use HARBOR|none the harbor to work in
|
|
66
68
|
harbor theme [auto|light|dark] the app's colours
|
|
67
69
|
harbor --version which version this is
|
|
@@ -76,7 +78,9 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
76
78
|
out, past 50 is getting it done
|
|
77
79
|
harbor mine what's on you, late first
|
|
78
80
|
harbor tasks [words] [--project P] [--assignee me|none|NAME]
|
|
79
|
-
[--late] [--status S,S]
|
|
81
|
+
[--late] [--status S,S] [--all]
|
|
82
|
+
the first 20 (--all: every one);
|
|
83
|
+
done ones only with --status done
|
|
80
84
|
harbor show TASK one task (TASK: a key like RR-42)
|
|
81
85
|
harbor open [TASK|PROJECT] open it on the website (or Today)
|
|
82
86
|
harbor invite EMAIL [--viewer] invite someone to your harbor
|
|
@@ -122,9 +126,12 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
122
126
|
harbor risks [words] [--kind risk,issue,decision] [--project P] [--closed]
|
|
123
127
|
the register, worst first
|
|
124
128
|
(--closed adds closed ones)
|
|
125
|
-
harbor brief [--portfolio P] [--say]
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
harbor brief [--portfolio P] [--all] [--say]
|
|
130
|
+
the weekly brief, ready to send
|
|
131
|
+
(--all lists every late and done
|
|
132
|
+
task); --say reads it aloud as a
|
|
133
|
+
shipping forecast (on Linux it
|
|
134
|
+
needs espeak-ng)
|
|
128
135
|
harbor report PROGRAM a program's status report
|
|
129
136
|
harbor export tasks PROJECT|risks|review [--all]
|
|
130
137
|
a spreadsheet (CSV) to stdout
|
|
@@ -1078,6 +1085,10 @@ export async function run(argv, io = {}) {
|
|
|
1078
1085
|
: harbors(r),
|
|
1079
1086
|
);
|
|
1080
1087
|
}
|
|
1088
|
+
case "whoami": {
|
|
1089
|
+
none(parse(spec()).positionals);
|
|
1090
|
+
return call("list_harbors", {}, (r) => whoami(r, creds));
|
|
1091
|
+
}
|
|
1081
1092
|
case "use": {
|
|
1082
1093
|
const { positionals } = parse({ flags: ["json"], options: [] });
|
|
1083
1094
|
const wanted = positionals.join(" ").trim();
|
|
@@ -1228,25 +1239,40 @@ export async function run(argv, io = {}) {
|
|
|
1228
1239
|
}
|
|
1229
1240
|
case "tasks": {
|
|
1230
1241
|
const { positionals, values, harbor } = parse(
|
|
1231
|
-
spec(["late"], ["project", "assignee", "status"]),
|
|
1232
|
-
);
|
|
1233
|
-
return call(
|
|
1234
|
-
"search_tasks",
|
|
1235
|
-
{
|
|
1236
|
-
harbor,
|
|
1237
|
-
query: positionals.join(" ") || undefined,
|
|
1238
|
-
project: values.project,
|
|
1239
|
-
assignee: values.assignee,
|
|
1240
|
-
status: values.status
|
|
1241
|
-
? values.status
|
|
1242
|
-
.split(",")
|
|
1243
|
-
.map((x) => x.trim())
|
|
1244
|
-
.filter(Boolean)
|
|
1245
|
-
: undefined,
|
|
1246
|
-
overdue: values.late || undefined,
|
|
1247
|
-
},
|
|
1248
|
-
(r) => taskList(r, opts),
|
|
1242
|
+
spec(["late", "all"], ["project", "assignee", "status"]),
|
|
1249
1243
|
);
|
|
1244
|
+
const args = {
|
|
1245
|
+
harbor,
|
|
1246
|
+
query: positionals.join(" ") || undefined,
|
|
1247
|
+
project: values.project,
|
|
1248
|
+
assignee: values.assignee,
|
|
1249
|
+
status: values.status
|
|
1250
|
+
? values.status
|
|
1251
|
+
.split(",")
|
|
1252
|
+
.map((x) => x.trim())
|
|
1253
|
+
.filter(Boolean)
|
|
1254
|
+
: undefined,
|
|
1255
|
+
overdue: values.late || undefined,
|
|
1256
|
+
};
|
|
1257
|
+
if (!values.all) return call("search_tasks", args, (r) => taskList(r, opts));
|
|
1258
|
+
// --all pages through every match, PAGE at a time (search_tasks' most).
|
|
1259
|
+
const PAGE = 50;
|
|
1260
|
+
const tasks = [];
|
|
1261
|
+
let first;
|
|
1262
|
+
do {
|
|
1263
|
+
const page = await client
|
|
1264
|
+
.call("search_tasks", { ...args, limit: PAGE, offset: tasks.length })
|
|
1265
|
+
.catch((err) => {
|
|
1266
|
+
if (err instanceof HarborError) throw new HarborError(hint(err.message));
|
|
1267
|
+
throw err;
|
|
1268
|
+
});
|
|
1269
|
+
first ??= page;
|
|
1270
|
+
tasks.push(...(page.tasks ?? []));
|
|
1271
|
+
if (!page.tasks?.length) break;
|
|
1272
|
+
} while (tasks.length < first.total);
|
|
1273
|
+
const result = { ...first, total: tasks.length, tasks };
|
|
1274
|
+
out(asJson ? jsonText(result) : plain(taskList(result, opts)));
|
|
1275
|
+
return 0;
|
|
1250
1276
|
}
|
|
1251
1277
|
case "show": {
|
|
1252
1278
|
const { positionals, harbor } = parse(spec());
|
|
@@ -1535,10 +1561,13 @@ export async function run(argv, io = {}) {
|
|
|
1535
1561
|
);
|
|
1536
1562
|
}
|
|
1537
1563
|
case "brief": {
|
|
1538
|
-
const { positionals, values, harbor } = parse(spec(["say"], ["portfolio"]));
|
|
1564
|
+
const { positionals, values, harbor } = parse(spec(["say", "all"], ["portfolio"]));
|
|
1539
1565
|
none(positionals);
|
|
1540
1566
|
if (!values.say) {
|
|
1541
|
-
|
|
1567
|
+
const args = { harbor, portfolio: values.portfolio, ...(values.all ? { all: true } : {}) };
|
|
1568
|
+
return call("weekly_brief", args, (r) =>
|
|
1569
|
+
r.hidden ? `${r.text}\n\nharbor brief --all lists every one.` : r.text,
|
|
1570
|
+
);
|
|
1542
1571
|
}
|
|
1543
1572
|
const result = await client
|
|
1544
1573
|
.call("shipping_forecast", { harbor, portfolio: values.portfolio, at: localMinute() })
|
|
@@ -1546,8 +1575,13 @@ export async function run(argv, io = {}) {
|
|
|
1546
1575
|
if (err instanceof HarborError) throw new HarborError(hint(err.message));
|
|
1547
1576
|
throw err;
|
|
1548
1577
|
});
|
|
1578
|
+
// --json is for a script reading the forecast, so nothing is spoken.
|
|
1579
|
+
if (asJson) {
|
|
1580
|
+
out(jsonText(result));
|
|
1581
|
+
return 0;
|
|
1582
|
+
}
|
|
1549
1583
|
const script = plain(result.script ?? "");
|
|
1550
|
-
out(
|
|
1584
|
+
out(script);
|
|
1551
1585
|
if (!(await (io.speak ?? speak)(script))) {
|
|
1552
1586
|
throw new HarborError(
|
|
1553
1587
|
process.platform === "linux"
|
|
@@ -1652,7 +1686,12 @@ export async function run(argv, io = {}) {
|
|
|
1652
1686
|
: `Added your answer (${r.answered.yourAnswers} so far).`,
|
|
1653
1687
|
);
|
|
1654
1688
|
}
|
|
1655
|
-
return call(
|
|
1689
|
+
return call(
|
|
1690
|
+
"get_premortem",
|
|
1691
|
+
{ harbor, project },
|
|
1692
|
+
(r) => premortemText(r, project),
|
|
1693
|
+
premortemForTerminal,
|
|
1694
|
+
);
|
|
1656
1695
|
}
|
|
1657
1696
|
case "limit": {
|
|
1658
1697
|
const { positionals, harbor } = parse(spec());
|
package/lib/format.mjs
CHANGED
|
@@ -63,12 +63,15 @@ export function myWork(w, opts) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export function taskList(r, opts) {
|
|
66
|
-
|
|
66
|
+
const done = r.doneMatches
|
|
67
|
+
? `${r.doneMatches} done task${r.doneMatches === 1 ? "" : "s"} match${r.doneMatches === 1 ? "es" : ""} too; add --status done to see ${r.doneMatches === 1 ? "it" : "them"}.`
|
|
68
|
+
: "";
|
|
69
|
+
if (!r.tasks?.length) return done ? `No open tasks match. ${done}` : "No tasks match.";
|
|
67
70
|
const more =
|
|
68
71
|
r.total > r.tasks.length
|
|
69
|
-
? `\n…and ${r.total - r.tasks.length} more.
|
|
72
|
+
? `\n…and ${r.total - r.tasks.length} more. Add --all for every one, or narrow it with a word or --project.`
|
|
70
73
|
: "";
|
|
71
|
-
return r.tasks.map((t) => taskLine(t, opts)).join("\n") + more;
|
|
74
|
+
return r.tasks.map((t) => taskLine(t, opts)).join("\n") + more + (done ? `\n${done}` : "");
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
const HEALTH = { "on-track": "on track", "at-risk": "at risk", "off-track": "off track" };
|
|
@@ -166,6 +169,28 @@ export function limitsText(name, limits, tasks = {}, hidden = []) {
|
|
|
166
169
|
return `${name}: ${capped.join(", ")}${rest}.${offNote}`;
|
|
167
170
|
}
|
|
168
171
|
|
|
172
|
+
/**
|
|
173
|
+
* harbor whoami: the account this machine signed in as, where, and the
|
|
174
|
+
* harbor commands use. From list_harbors (which names the account) and the
|
|
175
|
+
* saved sign-in.
|
|
176
|
+
*/
|
|
177
|
+
export function whoami(r, creds) {
|
|
178
|
+
const a = r.account;
|
|
179
|
+
const who = a?.name && a?.email ? `${a.name} (${a.email})` : (a?.email ?? a?.name ?? null);
|
|
180
|
+
const reading = creds.scope?.split(" ").includes("harbor:write") ? "" : ", reading only";
|
|
181
|
+
const lines = [`${who ? `Signed in as ${who}` : "Signed in"} to ${creds.server}${reading}.`];
|
|
182
|
+
const list = r.harbors ?? [];
|
|
183
|
+
const set = creds.harbor ? list.find((h) => h.id === creds.harbor) : null;
|
|
184
|
+
if (set) lines.push(`Commands use ${set.name} (harbor use).`);
|
|
185
|
+
else if (creds.harbor)
|
|
186
|
+
lines.push(`Commands use ${creds.harborName ?? creds.harbor}, which you're no longer in.`);
|
|
187
|
+
else if (list.length === 1) lines.push(`Commands use ${list[0].name}, your only harbor.`);
|
|
188
|
+
else if (list.length)
|
|
189
|
+
lines.push("No harbor set: say --harbor NAME, or set one with harbor use NAME.");
|
|
190
|
+
else lines.push("You aren't in any harbor yet. Run harbor to make one.");
|
|
191
|
+
return lines.join("\n");
|
|
192
|
+
}
|
|
193
|
+
|
|
169
194
|
export function harbors(r) {
|
|
170
195
|
const list = r.harbors ?? [];
|
|
171
196
|
if (!list.length) return "You aren't in any harbor yet. Run harbor to make one.";
|
|
@@ -305,10 +330,10 @@ export function forecastData(r) {
|
|
|
305
330
|
return { projects: r.projects.filter((p) => p.health !== "done").map(one) };
|
|
306
331
|
}
|
|
307
332
|
|
|
308
|
-
/** A project's pre-mortem, as get_premortem answers it. */
|
|
309
|
-
export function premortemText(r) {
|
|
333
|
+
/** A project's pre-mortem, as get_premortem answers it. The commands it suggests name the project as it was typed (a key or id when its name is shared). */
|
|
334
|
+
export function premortemText(r, project = r.project) {
|
|
310
335
|
if (!r.running) {
|
|
311
|
-
return `No pre-mortem on ${r.project} yet. Start one with harbor premortem "${
|
|
336
|
+
return `No pre-mortem on ${r.project} yet. Start one with harbor premortem "${project}" --start.`;
|
|
312
337
|
}
|
|
313
338
|
const count = `${r.answerCount} answer${r.answerCount === 1 ? "" : "s"}`;
|
|
314
339
|
const head = [`Pre-mortem · ${r.project} · ${count}`, r.question];
|
|
@@ -329,7 +354,7 @@ export function premortemText(r) {
|
|
|
329
354
|
return ` ${a.yours ? "*" : "-"} ${a.answer}${after}`;
|
|
330
355
|
});
|
|
331
356
|
if (!lines.length)
|
|
332
|
-
lines.push(` Nothing yet. Add one: harbor premortem "${
|
|
357
|
+
lines.push(` Nothing yet. Add one: harbor premortem "${project}" "WHAT WENT WRONG"`);
|
|
333
358
|
return [...head, "", ...lines].join("\n");
|
|
334
359
|
}
|
|
335
360
|
|