@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/README.md +10 -4
- package/dist/app.mjs +955 -51
- package/lib/cli.mjs +166 -49
- package/lib/format.mjs +34 -5
- package/package.json +1 -1
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 [
|
|
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]
|
|
227
|
-
weeks,
|
|
228
|
-
|
|
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"]
|
|
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]
|
|
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
|
-
"
|
|
784
|
-
"
|
|
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
|
-
|
|
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 =
|
|
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)
|
|
2023
|
-
|
|
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
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
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)
|
|
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) =>
|
|
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)
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
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
|
-
|
|
2564
|
-
if (
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
-
"
|
|
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).",
|