@harborppm/cli 0.39.0 → 0.40.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 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] the weekly brief, ready to send
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, 8)) lines.push(`• ${t.title}${t.project ? ` (${t.project})` : ""}`);
5085
- if (opts.shipped.length > 8) lines.push(`• and ${opts.shipped.length - 8} more`);
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, 8)) {
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 > 8) lines.push(`• and ${opts.late.length - 8} more`);
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] the weekly brief, ready to send;
126
- --say reads it aloud as a
127
- shipping forecast
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
- return call("weekly_brief", { harbor, portfolio: values.portfolio }, (r) => r.text);
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(asJson ? jsonText(result) : script);
1584
+ out(script);
1551
1585
  if (!(await (io.speak ?? speak)(script))) {
1552
1586
  throw new HarborError(
1553
1587
  process.platform === "linux"
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
- if (!r.tasks?.length) return "No tasks match.";
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. Narrow it with a word or --project.`
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.";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "description": "Harbor from the terminal: a full-screen app with everything, plus commands for scripts and agents.",
5
5
  "keywords": [
6
6
  "harbor",