@harborppm/cli 0.57.0 → 0.59.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/lib/cli.mjs CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  lifeboatsText,
40
40
  followedText,
41
41
  soundingsText,
42
+ ashoreText,
42
43
  voyageText,
43
44
  whyText,
44
45
  handoverText,
@@ -213,20 +214,27 @@ export const HELP = `harbor: Harbor from the terminal
213
214
  harbor note PROJECT ["NOTE"] [--opens DATE|--milestone NAME|--off-track]
214
215
  a note for later: sealed until
215
216
  its day, milestone, or Off track
216
- harbor sound TASK [DAYS] [--why TEXT] [--ask NAMES|crew] [--settle DAYS]
217
+ harbor sound TASK [--answer DAYS|--ask NAMES|--settle DAYS] [--why TEXT]
217
218
  a sounding: ask a few people how
218
219
  many days a task will take, give
219
220
  your answer, or read the spread
220
221
  harbor soundings [PROJECT] open tasks with a sounding, and
221
222
  the ones waiting on your answer
223
+ harbor ashore [--weeks N] [--country CODES|none] [--person NAME]
224
+ public holidays and time away
225
+ coming up, and whose holidays
226
+ the harbor (or someone) takes
222
227
  harbor handover PERSON [--to NAME] [--until DATE] [--note TEXT]
223
228
  [--keep ID,ID] [--confirm]
224
229
  hand someone's work to the crew
225
230
  harbor handback PERSON give it back after time away
226
- harbor fog [--project P] [--list] tasks nobody has touched for three
227
- weeks, one at a time: still
228
- needed, a new date, or drop it
231
+ harbor fog [--project P] tasks nobody has touched for three
232
+ weeks, longest first
233
+ harbor fog --sweep [--project P] one at a time: still needed, a
234
+ new date, or drop it
229
235
  harbor fog keep TASK it's still needed: out of the fog
236
+ harbor watch [--project P] [--bell] a live ticker of the harbor's
237
+ changes; yours bright, q stops
230
238
  harbor lifeboats [--program P] work only one person knows, each
231
239
  with a second pair of hands
232
240
  harbor lifeboats N [--person NAME] adds that second pair of hands
@@ -244,7 +252,7 @@ export const HELP = `harbor: Harbor from the terminal
244
252
  DATE: YYYY-MM-DD, today, tomorrow, or +N for N days from today.
245
253
  Changes show in Harbor's History as yours, "via Harbor CLI". These commands
246
254
  never delete; the full-screen app can, as the website can, and asks first,
247
- and so can harbor fog's x (drop it), one task at a time.
255
+ and so can harbor fog --sweep's x (drop it), one task at a time.
248
256
  `;
249
257
 
250
258
  export const VERSION = JSON.parse(
@@ -446,6 +454,8 @@ const FLAG = {
446
454
  days: "the days",
447
455
  why: "--why",
448
456
  people: "--ask",
457
+ countries: "--country",
458
+ country: "--country",
449
459
  };
450
460
 
451
461
  /** Harbor's refusals are worded for agents; say them as the commands and flags that answer them. */
@@ -728,7 +738,8 @@ const DETAIL = {
728
738
  " harbor sound TASK --ask crew writes: asks you, its owner and whoever",
729
739
  " has open work on its project",
730
740
  ' harbor sound TASK --ask "Sam,Priya" writes: asks those people',
731
- ' harbor sound TASK 4 [--why "TEXT"] writes: your answer, in working days',
741
+ ' harbor sound TASK --answer 4 [--why "TEXT"]',
742
+ " writes: your answer, in working days",
732
743
  " harbor sound TASK --settle 6 writes: the days you agreed on after",
733
744
  " talking a wide spread through",
734
745
  "",
@@ -736,6 +747,19 @@ const DETAIL = {
736
747
  " everyone has. Close guesses give the task an estimate; far-apart ones",
737
748
  " say talk first. The forecast draws on every answer.",
738
749
  ],
750
+ ashore: [
751
+ " harbor ashore [--weeks N] reads: holidays and time away in the",
752
+ " next N weeks (8 unless you say)",
753
+ " harbor ashore --country US,GB writes: whose public holidays the",
754
+ " harbor takes (none for no holidays)",
755
+ " harbor ashore --person NAME --country GB",
756
+ " writes: one person's own country",
757
+ " (harbor to take the harbor's again)",
758
+ "",
759
+ " The forecast, the weeks ahead, the timeline and calendar feeds count",
760
+ " these as days nobody works. Countries: AU BR CA DE ES FR GB IE IT MX NL",
761
+ " NZ US (national holidays only).",
762
+ ],
739
763
  soundings: [
740
764
  " harbor soundings [PROJECT] reads: open tasks with a sounding",
741
765
  " (all projects when left out), and the",
@@ -770,18 +794,29 @@ const DETAIL = {
770
794
  ' PERSON is a name, an id, or "me".',
771
795
  ],
772
796
  fog: [
773
- " harbor fog [--project P] in a terminal: the tasks in fog, one",
797
+ " harbor fog [--project P] reads them: open tasks nobody has",
798
+ " opened, moved, edited or commented on",
799
+ " for three weeks, longest first",
800
+ " harbor fog --sweep [--project P] in a terminal: the tasks in fog, one",
774
801
  " at a time. k still needed (clears the",
775
802
  " fog), d a new date, x drop it, s skip,",
776
803
  " q stop; then it saves what you chose",
777
- " harbor fog --list reads them: open tasks nobody has",
778
- " opened, moved, edited or commented on",
779
- " for three weeks, longest first",
780
804
  " harbor fog keep TASK writes: it's still needed, so it",
781
805
  " leaves the fog",
782
806
  "",
783
- " Piped, or with --json, it lists. The one-at-a-time sweep needs full access",
784
- " (harbor login without --read-only), since dropping a task deletes it.",
807
+ " The sweep needs a terminal and full access (harbor login without",
808
+ " --read-only), since dropping a task deletes it.",
809
+ ],
810
+ watch: [
811
+ " harbor watch reads, live: each change to the harbor",
812
+ " as it lands, one line with the time:",
813
+ " cards moving, comments, health changes",
814
+ " --project P one project's changes only",
815
+ " --bell ring the terminal's bell when something of yours changes",
816
+ "",
817
+ " Lines about your own work (your tasks, mentions of you, what you lead) are",
818
+ " bright; the rest are dim. q or Ctrl+C stops. Made for a narrow split or a",
819
+ " tmux pane; it needs full access (harbor login without --read-only).",
785
820
  ],
786
821
  lifeboats: [
787
822
  " harbor lifeboats [--program P] reads: work only one person has",
@@ -863,7 +898,7 @@ const DETAIL = {
863
898
  ' harbor raid "TITLE" --update "TEXT" writes: adds a line to its updates',
864
899
  ' harbor raid "TITLE" --still-stands "WHY"',
865
900
  " writes: a decision looked at again",
866
- " still stands (\"\" for no reason);",
901
+ ' still stands ("" for no reason);',
867
902
  " clears its look again",
868
903
  "",
869
904
  " Also --owner me|none|NAME, --impact 1-5, --probability 1-5 (risks),",
@@ -2010,7 +2045,8 @@ export async function run(argv, io = {}) {
2010
2045
  );
2011
2046
  if (!values.project) throw new HarborError("Say which project: --project NAME.");
2012
2047
  // Several at once: one title a line on stdin, with no title (or -) after add.
2013
- const piped = positionals.length === 0 || (positionals.length === 1 && positionals[0] === "-");
2048
+ const piped =
2049
+ positionals.length === 0 || (positionals.length === 1 && positionals[0] === "-");
2014
2050
  const stdin = io.stdin ?? process.stdin;
2015
2051
  const titles =
2016
2052
  piped && (io.stdin || !process.stdin.isTTY)
@@ -2019,28 +2055,34 @@ export async function run(argv, io = {}) {
2019
2055
  .map((l) => l.trim())
2020
2056
  .filter(Boolean)
2021
2057
  : [text(positionals, "the task")];
2022
- if (titles.length === 0) throw new HarborError("Say the task, or give one title a line on stdin.");
2023
- if (titles.length > 100) throw new HarborError("That's more than 100 tasks; harbor import FILE takes a spreadsheet.");
2058
+ if (titles.length === 0)
2059
+ throw new HarborError("Say the task, or give one title a line on stdin.");
2060
+ if (titles.length > 100)
2061
+ throw new HarborError(
2062
+ "That's more than 100 tasks; harbor import FILE takes a spreadsheet.",
2063
+ );
2024
2064
  const added = [];
2025
2065
  for (const title of titles) {
2026
- const r = await client.call("create_task", {
2027
- harbor,
2028
- project: values.project,
2029
- title,
2030
- // Yours unless you say otherwise, as with n in the app.
2031
- assignee: values.assignee ?? "me",
2032
- due: duePilot(values.due) ? undefined : dueDate(values.due),
2033
- suggestedDue: duePilot(values.due),
2034
- priority: values.priority,
2035
- status: values.status === undefined ? undefined : statusWord(values.status),
2036
- overLimit: values["over-limit"] || undefined,
2037
- }).catch((err) => {
2038
- if (err instanceof HarborError) {
2039
- const done = added.length ? ` (added ${added.length} before it)` : "";
2040
- throw new HarborError(`${hint(err.message, values.project)}${done}`);
2041
- }
2042
- throw err;
2043
- });
2066
+ const r = await client
2067
+ .call("create_task", {
2068
+ harbor,
2069
+ project: values.project,
2070
+ title,
2071
+ // Yours unless you say otherwise, as with n in the app.
2072
+ assignee: values.assignee ?? "me",
2073
+ due: duePilot(values.due) ? undefined : dueDate(values.due),
2074
+ suggestedDue: duePilot(values.due),
2075
+ priority: values.priority,
2076
+ status: values.status === undefined ? undefined : statusWord(values.status),
2077
+ overLimit: values["over-limit"] || undefined,
2078
+ })
2079
+ .catch((err) => {
2080
+ if (err instanceof HarborError) {
2081
+ const done = added.length ? ` (added ${added.length} before it)` : "";
2082
+ throw new HarborError(`${hint(err.message, values.project)}${done}`);
2083
+ }
2084
+ throw err;
2085
+ });
2044
2086
  if (values.comment)
2045
2087
  r.comment = (
2046
2088
  await client.call("add_comment", {
@@ -2195,19 +2237,26 @@ export async function run(argv, io = {}) {
2195
2237
  const number = (v, name) => {
2196
2238
  if (v === undefined) return undefined;
2197
2239
  const n = Number(v);
2198
- if (!Number.isFinite(n) || n <= 0) throw new HarborError(`--${name} is a number of hours, like 2 or 1.5.`);
2240
+ if (!Number.isFinite(n) || n <= 0)
2241
+ throw new HarborError(`--${name} is a number of hours, like 2 or 1.5.`);
2199
2242
  return n;
2200
2243
  };
2201
2244
  const [task, day, ...extra] = positionals;
2202
2245
  if (extra.length) throw new HarborError("Give a task key and a day: harbor week RR-42 wed.");
2203
2246
  const dayHours = number(values["day-hours"], "day-hours");
2204
- const show = (r) => [r.done, weekText(r, { ...opts, review: Boolean(values.review) })].filter(Boolean).join("\n\n");
2247
+ const show = (r) =>
2248
+ [r.done, weekText(r, { ...opts, review: Boolean(values.review) })]
2249
+ .filter(Boolean)
2250
+ .join("\n\n");
2205
2251
  if (!task && dayHours === undefined) {
2206
- if (values.off || values.hours) throw new HarborError("Say which task: harbor week RR-42 wed --hours 3.");
2252
+ if (values.off || values.hours)
2253
+ throw new HarborError("Say which task: harbor week RR-42 wed --hours 3.");
2207
2254
  return call("get_week", { harbor, week }, show);
2208
2255
  }
2209
2256
  if (task && !day && !values.off)
2210
- throw new HarborError(`Say which day for ${task}: harbor week ${task} wed, or --off to take it off the week.`);
2257
+ throw new HarborError(
2258
+ `Say which day for ${task}: harbor week ${task} wed, or --off to take it off the week.`,
2259
+ );
2211
2260
  return call(
2212
2261
  "plan_week",
2213
2262
  {
@@ -2471,9 +2520,14 @@ export async function run(argv, io = {}) {
2471
2520
  );
2472
2521
  }
2473
2522
  case "sound": {
2474
- const { positionals, values, harbor } = parse(spec([], ["why", "ask", "settle"]));
2475
- const [task, days, ...extra] = positionals;
2523
+ const { positionals, values, harbor } = parse(spec([], ["answer", "why", "ask", "settle"]));
2524
+ const [task, ...extra] = positionals;
2476
2525
  if (!task) throw new HarborError("Give a task's key, like harbor sound RR-4.");
2526
+ // A bare number used to be the answer; it writes, so it now needs its flag.
2527
+ if (extra.length === 1 && /^\d+(\.\d+)?(d|days?)?$/i.test(extra[0]))
2528
+ throw new HarborError(
2529
+ `To give your answer, say so: harbor sound ${task} --answer ${extra[0]}.`,
2530
+ );
2477
2531
  if (extra.length) throw new HarborError(`Unexpected "${extra.join(" ")}".`);
2478
2532
  const number = (text) => {
2479
2533
  const n = Number(String(text).replace(/d(ays?)?$/i, ""));
@@ -2481,11 +2535,14 @@ export async function run(argv, io = {}) {
2481
2535
  throw new HarborError(`Give days as a number, like 3 or 2.5 (not "${text}").`);
2482
2536
  return n;
2483
2537
  };
2538
+ const days = values.answer;
2484
2539
  const given = [days !== undefined, values.ask !== undefined, values.settle !== undefined];
2485
2540
  if (given.filter(Boolean).length > 1)
2486
2541
  throw new HarborError("Answer, ask or settle: one at a time.");
2487
2542
  if (values.why !== undefined && days === undefined)
2488
- throw new HarborError('--why goes with your answer: harbor sound TASK 4 --why "TEXT".');
2543
+ throw new HarborError(
2544
+ '--why goes with your answer: harbor sound TASK --answer 4 --why "TEXT".',
2545
+ );
2489
2546
  if (values.ask !== undefined) {
2490
2547
  const crew = /^crew$/i.test(values.ask.trim())
2491
2548
  ? undefined
@@ -2493,11 +2550,10 @@ export async function run(argv, io = {}) {
2493
2550
  .split(",")
2494
2551
  .map((x) => x.trim())
2495
2552
  .filter(Boolean);
2496
- return call(
2497
- "take_sounding",
2498
- { harbor, task, ...(crew ? { people: crew } : {}) },
2499
- (r) =>
2500
- `Asked ${r.asked.people.join(", ")} how many days ${r.asked.key ?? r.asked.title} will take. The answers show together once everyone's in.`,
2553
+ return call("take_sounding", { harbor, task, ...(crew ? { people: crew } : {}) }, (r) =>
2554
+ !crew && r.asked.people.length === 1
2555
+ ? `Only you are on this crew, so the sounding on ${r.asked.key ?? r.asked.title} is just your guess. Ask others by name: harbor sound ${task} --ask "Sam,Priya".`
2556
+ : `Asked ${r.asked.people.join(", ")} how many days ${r.asked.key ?? r.asked.title} will take. The answers show together once everyone's in.`,
2501
2557
  );
2502
2558
  }
2503
2559
  if (values.settle !== undefined) {
@@ -2529,6 +2585,39 @@ export async function run(argv, io = {}) {
2529
2585
  soundingsText(r),
2530
2586
  );
2531
2587
  }
2588
+ case "ashore": {
2589
+ const { positionals, values, harbor } = parse(spec([], ["weeks", "country", "person"]));
2590
+ none(positionals);
2591
+ if (values.person && values.country === undefined)
2592
+ throw new HarborError("Give their country too: --person NAME --country GB (or harbor).");
2593
+ if (values.country !== undefined) {
2594
+ if (values.person) {
2595
+ return call(
2596
+ "set_holidays",
2597
+ { harbor, person: values.person, country: values.country },
2598
+ (r) =>
2599
+ r.country === "harbor"
2600
+ ? `${r.person} takes the harbor's holidays.`
2601
+ : `${r.person} takes ${r.country} holidays.`,
2602
+ );
2603
+ }
2604
+ const countries = /^none$/i.test(values.country.trim())
2605
+ ? []
2606
+ : values.country
2607
+ .split(",")
2608
+ .map((c) => c.trim())
2609
+ .filter(Boolean);
2610
+ return call("set_holidays", { harbor, countries }, (r) =>
2611
+ r.harbor.length
2612
+ ? `The harbor takes public holidays from ${r.harbor.map((c) => c.name).join(", ")}.`
2613
+ : "The harbor takes no public holidays now.",
2614
+ );
2615
+ }
2616
+ const weeks = values.weeks === undefined ? 8 : Number(values.weeks);
2617
+ if (!Number.isInteger(weeks) || weeks < 1 || weeks > 52)
2618
+ throw new HarborError("--weeks is a whole number from 1 to 52.");
2619
+ return call("time_ashore", { harbor, days: weeks * 7 }, (r) => ashoreText(r, weeks));
2620
+ }
2532
2621
  case "why": {
2533
2622
  const { positionals, harbor } = parse(spec());
2534
2623
  const of = positionals.join(" ").trim();
@@ -2549,8 +2638,27 @@ export async function run(argv, io = {}) {
2549
2638
  }
2550
2639
  return call("get_voyage_log", { harbor, project }, voyageText, voyageForTerminal);
2551
2640
  }
2641
+ case "watch": {
2642
+ const { positionals, values, harbor } = parse(spec(["bell"], ["project"]));
2643
+ none(positionals);
2644
+ if (!hasFullAccess(creds))
2645
+ throw new HarborError(
2646
+ "Watching needs full access, as the full-screen app does. Run harbor login (without --read-only), then try again.",
2647
+ );
2648
+ const { watchHere } = await loadApp();
2649
+ await watchHere({
2650
+ source: tokenSource({ creds, save: (c) => writeCredentials(c, env), fetcher }),
2651
+ fetcher,
2652
+ harbor,
2653
+ saved: creds.harbor ?? null,
2654
+ project: values.project,
2655
+ bell: Boolean(values.bell),
2656
+ colour: colourDepth(io.stdout ?? process.stdout, env) !== "none",
2657
+ });
2658
+ return 0;
2659
+ }
2552
2660
  case "fog": {
2553
- const { positionals, values, harbor } = parse(spec(["list"], ["project"]));
2661
+ const { positionals, values, harbor } = parse(spec(["list", "sweep"], ["project"]));
2554
2662
  if (positionals[0] === "keep") {
2555
2663
  const task = one(positionals.slice(1), "task");
2556
2664
  return call(
@@ -2560,8 +2668,17 @@ export async function run(argv, io = {}) {
2560
2668
  );
2561
2669
  }
2562
2670
  none(positionals);
2563
- const terminal = io.terminal ?? Boolean(process.stdout.isTTY && process.stdin.isTTY);
2564
- if (terminal && !values.list && !asJson && hasFullAccess(creds)) {
2671
+ // Listing is the default; the sweep, where x deletes, only when asked for.
2672
+ if (values.sweep) {
2673
+ const terminal = io.terminal ?? Boolean(process.stdout.isTTY && process.stdin.isTTY);
2674
+ if (!terminal || asJson) {
2675
+ throw new HarborError("harbor fog --sweep asks about each task, so it needs a terminal.");
2676
+ }
2677
+ if (!hasFullAccess(creds)) {
2678
+ throw new HarborError(
2679
+ "harbor fog --sweep can drop tasks, so it needs full access: harbor login (without --read-only).",
2680
+ );
2681
+ }
2565
2682
  const { fogHere } = await loadApp();
2566
2683
  await fogHere({
2567
2684
  source: tokenSource({ creds, save: (c) => writeCredentials(c, env), fetcher }),
package/lib/format.mjs CHANGED
@@ -539,7 +539,10 @@ export function voyageText(r) {
539
539
 
540
540
  /** What's holding something up, as long_pole answers it: the headline, then the line of waits, first blocker at the top. */
541
541
  export function whyText(r) {
542
- const what = r.of.kind === "task" ? r.of.name : `${r.of.name}${r.of.kind === "milestone" ? " (milestone)" : ""}`;
542
+ const what =
543
+ r.of.kind === "task"
544
+ ? r.of.name
545
+ : `${r.of.name}${r.of.kind === "milestone" ? " (milestone)" : ""}`;
543
546
  if (r.none) return `What's holding up ${what}: ${r.none.replace(/^\w/, (c) => c.toLowerCase())}`;
544
547
  const head = [`What's holding up ${what}`];
545
548
  if (r.headline) head.push(r.headline);
@@ -552,14 +555,19 @@ export function whyText(r) {
552
555
  ` ${(l.key ?? "").padEnd(width)}${width ? " " : ""}${l.title}`,
553
556
  ` ${" ".repeat(width)}${width ? " " : ""}${l.assignee ?? "nobody yet"} · ${due}${finish} · ${l.slackText}`,
554
557
  ];
555
- return i < chain.length - 1 ? [...row, ` ${" ".repeat(width ? Math.floor(width / 2) : 0)}│`] : row;
558
+ return i < chain.length - 1
559
+ ? [...row, ` ${" ".repeat(width ? Math.floor(width / 2) : 0)}│`]
560
+ : row;
556
561
  });
557
562
  const via = r.via ? [`It waits longest on ${r.via.project}; inside it:`] : [];
558
563
  const others = (r.others ?? []).length
559
564
  ? [
560
565
  "",
561
566
  `Also waits on ${r.others
562
- .map((o) => `${o.project} (finishes ${o.finish}, ${o.slack >= 0 ? `${o.slack} day${o.slack === 1 ? "" : "s"} spare` : `${-o.slack} day${o.slack === -1 ? "" : "s"} over`})`)
567
+ .map(
568
+ (o) =>
569
+ `${o.project} (finishes ${o.finish}, ${o.slack >= 0 ? `${o.slack} day${o.slack === 1 ? "" : "s"} spare` : `${-o.slack} day${o.slack === -1 ? "" : "s"} over`})`,
570
+ )
563
571
  .join(", ")}.`,
564
572
  ]
565
573
  : [];
@@ -744,7 +752,7 @@ export function soundingText(task, s, { answered = false } = {}) {
744
752
  for (const a of s.answers)
745
753
  lines.push(` You said ${daysText(a.days)}${a.why ? ` · "${a.why}"` : ""}`);
746
754
  if (s.youWereAsked && !s.answers.length)
747
- lines.push(` Give yours: harbor sound ${task.key ?? task.id} DAYS`);
755
+ lines.push(` Give yours: harbor sound ${task.key ?? task.id} --answer DAYS`);
748
756
  return lines.join("\n");
749
757
  }
750
758
  lines.push(` ${spreadLine(s.answers)}`);
@@ -760,6 +768,26 @@ export function soundingText(task, s, { answered = false } = {}) {
760
768
  return lines.join("\n");
761
769
  }
762
770
 
771
+ /** time_ashore: whose holidays count, then the days off coming up, soonest first. */
772
+ export function ashoreText(r, weeks = 8) {
773
+ const lines = [];
774
+ const countries = r.countries ?? [];
775
+ lines.push(
776
+ countries.length
777
+ ? `Time ashore · holidays from ${countries.map((c) => c.name).join(", ")}`
778
+ : "Time ashore · no holiday country set (harbor ashore --country US)",
779
+ );
780
+ for (const p of r.people ?? []) lines.push(` ${p.name} takes ${p.country} holidays`);
781
+ const rows = [
782
+ ...(r.holidays ?? []).map((h) => [h.date, h.name]),
783
+ ...(r.away ?? []).map((a) => [a.from, `${a.name} away, back ${a.back}`]),
784
+ ].sort((a, b) => a[0].localeCompare(b[0]));
785
+ lines.push("");
786
+ if (!rows.length) lines.push(` Nothing in the next ${weeks} weeks.`);
787
+ for (const [day, what] of rows) lines.push(` ${day} ${what}`);
788
+ return lines.join("\n");
789
+ }
790
+
763
791
  /** list_soundings: the ones waiting on you first, then the rest, wide ones flagged. */
764
792
  export function soundingsText(r) {
765
793
  const all = r.soundings ?? [];
@@ -961,7 +989,7 @@ export function fogText(r, { sweepHint = true } = {}) {
961
989
  if (sweepHint)
962
990
  lines.push(
963
991
  "",
964
- "In a terminal, harbor fog walks through them one at a time: k still needed, d new date, x drop it. harbor fog keep TASK keeps one.",
992
+ "harbor fog --sweep walks through them one at a time: k still needed, d new date, x drop it. harbor fog keep TASK keeps one.",
965
993
  );
966
994
  return lines.join("\n");
967
995
  }
@@ -983,6 +1011,7 @@ export function lifeboatsText(r) {
983
1011
  });
984
1012
  if (r.more) lines.push("", `And ${r.count - r.lines.length} more.`);
985
1013
  for (const a of r.away ?? []) lines.push("", a);
1014
+ if (r.hint) return [...lines, "", r.hint].join("\n");
986
1015
  lines.push(
987
1016
  "",
988
1017
  "harbor lifeboats N adds the suggested second pair of hands (--person NAME for someone else).",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.57.0",
3
+ "version": "0.59.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",