@harborppm/cli 0.62.0 → 0.64.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 +855 -105
- package/lib/args.mjs +5 -4
- package/lib/cli.mjs +173 -22
- package/lib/format.mjs +8 -3
- package/package.json +1 -1
package/lib/args.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A small argument reader: positionals, `--flag`, `--key value` and
|
|
3
|
-
* `--key=value`.
|
|
3
|
+
* `--key=value`. An option named in `repeat` may be given more than once and
|
|
4
|
+
* reads as a list. Unknown flags are an error, so a typo doesn't quietly do
|
|
4
5
|
* something else.
|
|
5
6
|
*/
|
|
6
|
-
export function parseArgs(argv, { flags = [], options = [] } = {}) {
|
|
7
|
+
export function parseArgs(argv, { flags = [], options = [], repeat = [] } = {}) {
|
|
7
8
|
const positionals = [];
|
|
8
9
|
const values = {};
|
|
9
10
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -20,7 +21,7 @@ export function parseArgs(argv, { flags = [], options = [] } = {}) {
|
|
|
20
21
|
if (flags.includes(key)) {
|
|
21
22
|
if (inline !== undefined) throw new Error(`--${key} takes no value.`);
|
|
22
23
|
values[key] = true;
|
|
23
|
-
} else if (options.includes(key)) {
|
|
24
|
+
} else if (options.includes(key) || repeat.includes(key)) {
|
|
24
25
|
const value = inline ?? argv[++i];
|
|
25
26
|
if (
|
|
26
27
|
value === undefined ||
|
|
@@ -29,7 +30,7 @@ export function parseArgs(argv, { flags = [], options = [] } = {}) {
|
|
|
29
30
|
) {
|
|
30
31
|
throw new Error(`--${key} needs a value.`);
|
|
31
32
|
}
|
|
32
|
-
values[key] = value;
|
|
33
|
+
values[key] = repeat.includes(key) ? [...(values[key] ?? []), value] : value;
|
|
33
34
|
} else {
|
|
34
35
|
throw new Error(`Unknown option --${key}.`);
|
|
35
36
|
}
|
package/lib/cli.mjs
CHANGED
|
@@ -103,15 +103,23 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
103
103
|
harbor barnacles PROJECT work added since the plan was set:
|
|
104
104
|
by week, by who, and the days of
|
|
105
105
|
slip it explains
|
|
106
|
+
harbor baseline PROJECT [--reset] sets a project's baseline: its
|
|
107
|
+
dates now, and growth counted
|
|
108
|
+
from now
|
|
106
109
|
harbor hill [--program P] where each project is on its hill
|
|
107
110
|
harbor hill PROJECT --at 0-100|top|done move it: up to 50 is figuring it
|
|
108
111
|
out, past 50 is getting it done
|
|
109
112
|
harbor mine what's on you, late first
|
|
113
|
+
harbor clock [NAME] [--zone ZONE|here|none] [--hours 9-17]
|
|
114
|
+
who's working now, or set your
|
|
115
|
+
(or NAME's) time zone and hours
|
|
110
116
|
harbor tasks [words] [--project P] [--assignee me|none|NAME]
|
|
111
117
|
[--late] [--status S,S] [--all]
|
|
112
118
|
the first 20 (--all: every one);
|
|
113
119
|
done ones only with --status done
|
|
114
|
-
harbor show TASK one task (TASK: a key like RR-42)
|
|
120
|
+
harbor show TASK one task (TASK: a key like RR-42),
|
|
121
|
+
with the date its owner's queue
|
|
122
|
+
suggests
|
|
115
123
|
harbor open [TASK|PROJECT] open it on the website (or Today)
|
|
116
124
|
harbor invite EMAIL [--viewer] invite someone to your harbor
|
|
117
125
|
harbor prompt [--shell SHELL] [--refresh] due today and late, for your
|
|
@@ -223,9 +231,10 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
223
231
|
harbor voyage PROJECT ["YOUR LINE"] a Done project's voyage log, and
|
|
224
232
|
your line in it
|
|
225
233
|
harbor charter PROJECT [--why T] [--in T] [--out T] [--done-when T]
|
|
226
|
-
[--calls
|
|
227
|
-
a project's five-line charter
|
|
228
|
-
|
|
234
|
+
[--calls NAME]
|
|
235
|
+
a project's five-line charter
|
|
236
|
+
(why, in, out, done when, who
|
|
237
|
+
makes the calls): read or set
|
|
229
238
|
harbor note PROJECT ["NOTE"] [--opens DATE|--milestone NAME|--off-track]
|
|
230
239
|
a note for later: sealed until
|
|
231
240
|
its day, milestone, or Off track
|
|
@@ -288,9 +297,10 @@ function banner(io, env, words) {
|
|
|
288
297
|
const THEMES = ["auto", "light", "dark"];
|
|
289
298
|
|
|
290
299
|
const COMMON = { flags: ["json", "help"], options: ["harbor"] };
|
|
291
|
-
const spec = (flags = [], options = []) => ({
|
|
300
|
+
const spec = (flags = [], options = [], repeat = []) => ({
|
|
292
301
|
flags: [...COMMON.flags, ...flags],
|
|
293
302
|
options: [...COMMON.options, ...options],
|
|
303
|
+
repeat,
|
|
294
304
|
});
|
|
295
305
|
|
|
296
306
|
/**
|
|
@@ -566,9 +576,14 @@ const DETAIL = {
|
|
|
566
576
|
"",
|
|
567
577
|
" --assignee me|none|NAME, --due DATE|pilot, --priority low|medium|high|urgent,",
|
|
568
578
|
" --status S, --comment TEXT and --over-limit apply to every task added, so",
|
|
569
|
-
" ten lines with --comment add the same comment to all ten.
|
|
570
|
-
"
|
|
571
|
-
"
|
|
579
|
+
" ten lines with --comment add the same comment to all ten. More than 100",
|
|
580
|
+
" lines is harbor import's job.",
|
|
581
|
+
"",
|
|
582
|
+
" --due pilot takes the earliest day the owner's queue suggests: their open",
|
|
583
|
+
" work due first, their weekly pace, time away, what it waits on and how",
|
|
584
|
+
" long their similar tasks took. It needs an owner (--assignee none is",
|
|
585
|
+
" refused), and an empty queue gives their next working day. Several lines",
|
|
586
|
+
" each count the ones before them; with --dry-run it shows each line's date.",
|
|
572
587
|
],
|
|
573
588
|
week: [
|
|
574
589
|
" harbor week [--next|--last|--week DATE] reads: your week, the days and",
|
|
@@ -768,14 +783,21 @@ const DETAIL = {
|
|
|
768
783
|
charter: [
|
|
769
784
|
" harbor charter PROJECT reads its charter, and any open task",
|
|
770
785
|
" that names something it leaves out",
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
786
|
+
" Set any line, or all at once:",
|
|
787
|
+
' --why "TEXT" why the project exists',
|
|
788
|
+
' --in "A, B" what it covers (the voyage log counts the',
|
|
789
|
+
" finished tasks naming these)",
|
|
790
|
+
' --out "C, D" what it leaves out (tasks naming these show as',
|
|
791
|
+
" Out of scope?)",
|
|
792
|
+
' --done-when "TEXT" what finished looks like',
|
|
793
|
+
' --calls "NAME" who makes the calls when scope is in question',
|
|
774
794
|
"",
|
|
775
|
-
"
|
|
776
|
-
|
|
777
|
-
"
|
|
778
|
-
"
|
|
795
|
+
" Items in --in and --out are split on commas; give --in or --out again",
|
|
796
|
+
' for an item that has a comma in it: --out "Coupons" --out "Gift cards,',
|
|
797
|
+
' vouchers". none clears a line: --out none. The review lists running',
|
|
798
|
+
" projects still without a charter a week after they started. A charter",
|
|
799
|
+
" is for the harbor's members: share links and public roadmaps never show",
|
|
800
|
+
" it. PROJECT is a name, a key like RR, or an id.",
|
|
779
801
|
],
|
|
780
802
|
sound: [
|
|
781
803
|
" harbor sound TASK reads its sounding: who hasn't answered,",
|
|
@@ -859,7 +881,9 @@ const DETAIL = {
|
|
|
859
881
|
" --project P one project's changes only",
|
|
860
882
|
" --bell ring the terminal's bell when something of yours changes",
|
|
861
883
|
" --since TIME start with every change since TIME (14:30 today, or",
|
|
862
|
-
" 2026-10-21, or 2026-10-21T14:30) instead of the last 5",
|
|
884
|
+
" 2026-10-21, or 2026-10-21T14:30) instead of the last 5;",
|
|
885
|
+
" times are this computer's, or UTC with Z or UTC after",
|
|
886
|
+
" (14:30Z), and the header names the zone",
|
|
863
887
|
" --once print those and stop instead of listening",
|
|
864
888
|
" --json one JSON object a line (at, text, quote, mine, tone;",
|
|
865
889
|
" earlier: true for the ones from before it started)",
|
|
@@ -932,6 +956,30 @@ const DETAIL = {
|
|
|
932
956
|
" days before it (negative: that many over). PROJECT is a name, a key",
|
|
933
957
|
" like RR, or an id.",
|
|
934
958
|
],
|
|
959
|
+
clock: [
|
|
960
|
+
" harbor clock reads everyone's local time, and",
|
|
961
|
+
" who's working now",
|
|
962
|
+
" harbor clock NAME reads one person's",
|
|
963
|
+
" harbor clock [NAME] --zone ZONE writes: sets a time zone (yours",
|
|
964
|
+
" without NAME)",
|
|
965
|
+
" harbor clock [NAME] --hours 9-17 writes: sets working hours",
|
|
966
|
+
"",
|
|
967
|
+
" ZONE is an IANA name like America/New_York or Europe/London, here for",
|
|
968
|
+
" this machine's zone, or none to clear the zone and hours. --hours is",
|
|
969
|
+
" one start and end in that zone, like 9-17, 8:30-16:30 or 9am-5:30pm,",
|
|
970
|
+
" Monday to Friday: one span a day that ends after it starts, so no split",
|
|
971
|
+
" day or overnight shift. Without --hours a day runs 9 to 17. Agents",
|
|
972
|
+
" don't keep hours.",
|
|
973
|
+
],
|
|
974
|
+
baseline: [
|
|
975
|
+
" harbor baseline PROJECT writes: commits its start and end",
|
|
976
|
+
" dates as its baseline",
|
|
977
|
+
" harbor baseline PROJECT --reset writes: replaces one already set",
|
|
978
|
+
"",
|
|
979
|
+
" The timeline then shows slips against those dates, and harbor barnacles",
|
|
980
|
+
" counts tasks added from now on as growth. On the website a program's",
|
|
981
|
+
" Set baseline does the same for all its projects.",
|
|
982
|
+
],
|
|
935
983
|
barnacles: [
|
|
936
984
|
" harbor barnacles PROJECT reads how much the plan has grown",
|
|
937
985
|
"",
|
|
@@ -1291,6 +1339,7 @@ export async function run(argv, io = {}) {
|
|
|
1291
1339
|
const now = await readCredentials(env);
|
|
1292
1340
|
return now ? { ...c, harbor: now.harbor ?? null, harborName: now.harborName ?? null } : c;
|
|
1293
1341
|
};
|
|
1342
|
+
const settings = await readSettings(env);
|
|
1294
1343
|
await runApp({
|
|
1295
1344
|
source: tokenSource({
|
|
1296
1345
|
creds,
|
|
@@ -1304,9 +1353,20 @@ export async function run(argv, io = {}) {
|
|
|
1304
1353
|
const now = (await readCredentials(env)) ?? creds;
|
|
1305
1354
|
await writeCredentials({ ...now, harbor: h.id, harborName: h.name }, env);
|
|
1306
1355
|
},
|
|
1307
|
-
theme:
|
|
1356
|
+
theme: settings.theme,
|
|
1308
1357
|
browse: io.browse ?? openBrowser,
|
|
1309
1358
|
...(command === "bridge" ? { startIn: "bridge" } : {}),
|
|
1359
|
+
// When this machine last saw you in each harbor: the app opens on a
|
|
1360
|
+
// catch-up after a few days away (src/lib/welcome-back.ts).
|
|
1361
|
+
seen: {
|
|
1362
|
+
last: (id) => settings.seen?.[id] ?? null,
|
|
1363
|
+
mark: (id, at) => {
|
|
1364
|
+
settings.seen = { ...settings.seen, [id]: at };
|
|
1365
|
+
void readSettings(env)
|
|
1366
|
+
.then((now) => writeSettings({ ...now, seen: { ...now.seen, [id]: at } }, env))
|
|
1367
|
+
.catch(() => {});
|
|
1368
|
+
},
|
|
1369
|
+
},
|
|
1310
1370
|
});
|
|
1311
1371
|
return 0;
|
|
1312
1372
|
}
|
|
@@ -1900,6 +1960,8 @@ export async function run(argv, io = {}) {
|
|
|
1900
1960
|
),
|
|
1901
1961
|
];
|
|
1902
1962
|
if (t.repeats) lines.push(`Repeats ${REPEAT_WORD[t.repeats] ?? t.repeats}.`);
|
|
1963
|
+
if (t.pilotDue?.fits) lines.push(`${t.pilotDue.says}, so the due date holds.`);
|
|
1964
|
+
else if (t.pilotDue) lines.push(...pilotLines({ pilot: t.pilotDue }, t));
|
|
1903
1965
|
if (t.description) lines.push("", t.description);
|
|
1904
1966
|
if (t.checklist.length)
|
|
1905
1967
|
lines.push("", ...t.checklist.map((c) => ` [${c.done ? "x" : " "}] ${c.step}`));
|
|
@@ -2160,11 +2222,38 @@ export async function run(argv, io = {}) {
|
|
|
2160
2222
|
);
|
|
2161
2223
|
if (values["dry-run"]) {
|
|
2162
2224
|
const n = titles.length === 1 ? "1 task" : `${titles.length} tasks`;
|
|
2163
|
-
|
|
2225
|
+
// --due pilot: the date each line would take, each counting the ones before it.
|
|
2226
|
+
const pilot = duePilot(values.due)
|
|
2227
|
+
? (
|
|
2228
|
+
await client.call("suggest_due", {
|
|
2229
|
+
harbor,
|
|
2230
|
+
project: values.project,
|
|
2231
|
+
assignee: values.assignee ?? "me",
|
|
2232
|
+
count: titles.length,
|
|
2233
|
+
})
|
|
2234
|
+
).dates
|
|
2235
|
+
: null;
|
|
2236
|
+
if (asJson)
|
|
2237
|
+
out(
|
|
2238
|
+
jsonText({
|
|
2239
|
+
dryRun: true,
|
|
2240
|
+
project: values.project,
|
|
2241
|
+
titles,
|
|
2242
|
+
...(pilot ? { due: pilot.map((d) => d.earliest) } : {}),
|
|
2243
|
+
}),
|
|
2244
|
+
);
|
|
2164
2245
|
else
|
|
2165
2246
|
out(
|
|
2166
2247
|
plain(
|
|
2167
|
-
[
|
|
2248
|
+
[
|
|
2249
|
+
`Would add ${n} to ${values.project}:`,
|
|
2250
|
+
...titles.map((t, i) => ` ${t}${pilot ? ` (due ${pilot[i].earliest})` : ""}`),
|
|
2251
|
+
...(pilot
|
|
2252
|
+
? [
|
|
2253
|
+
`--due pilot: ${pilot[0].says}; ${pilot[0].why.map((w) => w.charAt(0).toLowerCase() + w.slice(1)).join("; ")}.`,
|
|
2254
|
+
]
|
|
2255
|
+
: []),
|
|
2256
|
+
].join("\n"),
|
|
2168
2257
|
),
|
|
2169
2258
|
);
|
|
2170
2259
|
return 0;
|
|
@@ -2393,6 +2482,51 @@ export async function run(argv, io = {}) {
|
|
|
2393
2482
|
`Comment added to ${r.commented.task}${r.mentioned?.length ? `; told ${r.mentioned.join(", ")}` : ""}.`,
|
|
2394
2483
|
);
|
|
2395
2484
|
}
|
|
2485
|
+
case "clock": {
|
|
2486
|
+
const { positionals, values, harbor } = parse(spec([], ["zone", "hours"]));
|
|
2487
|
+
const who = positionals.join(" ").trim();
|
|
2488
|
+
if (values.zone === undefined && values.hours === undefined) {
|
|
2489
|
+
return call("crew_clocks", { harbor }, (r) => {
|
|
2490
|
+
const rows = who ? r.clocks.filter((c) => nameHas(c.who, who)) : r.clocks;
|
|
2491
|
+
const quiet = who ? r.noTimeZone.filter((n) => nameHas(n, who)) : r.noTimeZone;
|
|
2492
|
+
if (rows.length === 0 && quiet.length === 0) {
|
|
2493
|
+
return who ? `Nobody on the crew matches "${who}".` : "Nobody on the crew yet.";
|
|
2494
|
+
}
|
|
2495
|
+
const width = Math.max(0, ...rows.map((c) => c.who.length));
|
|
2496
|
+
return [
|
|
2497
|
+
...rows.map(
|
|
2498
|
+
(c) =>
|
|
2499
|
+
`${c.who.padEnd(width)} ${c.localTime.padStart(5)} ${c.zone.replace(/_/g, " ")} ${c.working ? `working (${c.hours})` : (c.picksUp ?? "off")}`,
|
|
2500
|
+
),
|
|
2501
|
+
...(quiet.length
|
|
2502
|
+
? [
|
|
2503
|
+
`${rows.length ? "\n" : ""}No time zone yet: ${quiet.join(", ")}. harbor clock --zone here sets yours.`,
|
|
2504
|
+
]
|
|
2505
|
+
: []),
|
|
2506
|
+
].join("\n");
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
let zone = values.zone;
|
|
2510
|
+
if (zone?.toLowerCase() === "here") {
|
|
2511
|
+
zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2512
|
+
if (!zone) throw new HarborError("This machine doesn't say its time zone. Give one, like --zone Europe/London.");
|
|
2513
|
+
}
|
|
2514
|
+
let starts;
|
|
2515
|
+
let ends;
|
|
2516
|
+
if (values.hours !== undefined) {
|
|
2517
|
+
const m = /^\s*([^-–]+?)\s*[-–]\s*(.+?)\s*$/.exec(values.hours);
|
|
2518
|
+
if (!m) throw new HarborError("--hours is a start and an end, like 9-17 or 8:30-16:30.");
|
|
2519
|
+
[, starts, ends] = m;
|
|
2520
|
+
}
|
|
2521
|
+
return call(
|
|
2522
|
+
"set_clock",
|
|
2523
|
+
{ harbor, person: who || "me", zone, starts, ends },
|
|
2524
|
+
(r) =>
|
|
2525
|
+
r.zone
|
|
2526
|
+
? `${r.person}: ${r.zone.replace(/_/g, " ")}, ${r.hours} on weekdays. ${r.working ? `Working now (${r.localTime} there).` : (r.picksUp ?? "")}`.trim()
|
|
2527
|
+
: `${r.person} has no time zone now.`,
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2396
2530
|
case "checkin": {
|
|
2397
2531
|
const { positionals, values, harbor } = parse(spec(["on-blocker"]));
|
|
2398
2532
|
return call(
|
|
@@ -2594,11 +2728,15 @@ export async function run(argv, io = {}) {
|
|
|
2594
2728
|
}
|
|
2595
2729
|
case "charter": {
|
|
2596
2730
|
const { positionals, values, harbor } = parse(
|
|
2597
|
-
spec([], ["why", "
|
|
2731
|
+
spec([], ["why", "done-when", "calls"], ["in", "out"]),
|
|
2598
2732
|
);
|
|
2599
2733
|
const project = one(positionals, 'project: harbor charter "NAME"');
|
|
2600
|
-
// none clears a line, as --due none does.
|
|
2601
|
-
|
|
2734
|
+
// none clears a line, as --due none does. --in and --out given more than
|
|
2735
|
+
// once write one item each, as a bulleted list, so an item keeps its commas.
|
|
2736
|
+
const line = (v) => {
|
|
2737
|
+
if (Array.isArray(v)) v = v.length > 1 ? v.map((x) => `- ${x.trim()}`).join("\n") : v[0];
|
|
2738
|
+
return v.trim().toLowerCase() === "none" ? "" : v;
|
|
2739
|
+
};
|
|
2602
2740
|
const lines = Object.fromEntries(
|
|
2603
2741
|
[
|
|
2604
2742
|
["why", values.why],
|
|
@@ -2741,6 +2879,19 @@ export async function run(argv, io = {}) {
|
|
|
2741
2879
|
if (!project) throw new HarborError("Say which project: harbor barnacles PROJECT.");
|
|
2742
2880
|
return call("scope_growth", { harbor, project }, barnaclesText);
|
|
2743
2881
|
}
|
|
2882
|
+
case "baseline": {
|
|
2883
|
+
const { positionals, values, harbor } = parse(spec(["reset"]));
|
|
2884
|
+
const project = positionals.join(" ").trim();
|
|
2885
|
+
if (!project) throw new HarborError("Say which project: harbor baseline PROJECT.");
|
|
2886
|
+
return call(
|
|
2887
|
+
"set_baseline",
|
|
2888
|
+
{ harbor, project, ...(values.reset ? { reset: true } : {}) },
|
|
2889
|
+
(r) =>
|
|
2890
|
+
r.changed
|
|
2891
|
+
? `Set ${r.project}'s baseline: ${r.baselineStart} to ${r.baselineEnd}. harbor barnacles counts tasks added from now on.`
|
|
2892
|
+
: `${r.project} already has a baseline (${r.baselineStart} to ${r.baselineEnd}); --reset replaces it.`,
|
|
2893
|
+
);
|
|
2894
|
+
}
|
|
2744
2895
|
case "voyage": {
|
|
2745
2896
|
const { positionals, harbor } = parse(spec());
|
|
2746
2897
|
const [project, ...words] = positionals;
|
package/lib/format.mjs
CHANGED
|
@@ -635,6 +635,7 @@ export function barnaclesText(r) {
|
|
|
635
635
|
}
|
|
636
636
|
head.push(`${r.headline} (${r.planned} planned, ${r.addedThisWeek} added this week).`);
|
|
637
637
|
if (r.daysLine) head.push(r.daysLine);
|
|
638
|
+
if (r.caveat) head.push(r.caveat);
|
|
638
639
|
const top = Math.max(1, ...r.byWeek.map((w) => w.added));
|
|
639
640
|
const weeks = r.byWeek.map(
|
|
640
641
|
(w) => ` w/c ${w.weekOf} ${"█".repeat(Math.max(1, Math.round((w.added / top) * 20)))} ${w.added}`,
|
|
@@ -643,7 +644,7 @@ export function barnaclesText(r) {
|
|
|
643
644
|
const from =
|
|
644
645
|
r.measuredFrom === "when the baseline was set"
|
|
645
646
|
? `Counted from when the baseline was set, ${r.since.slice(0, 10)}.`
|
|
646
|
-
: `Counted from a week after its first task was made, ${r.since.slice(0, 10)};
|
|
647
|
+
: `Counted from a week after its first task was made, ${r.since.slice(0, 10)}; harbor baseline "${r.project}" counts from then instead.`;
|
|
647
648
|
return [...head, "", "Added by week:", ...weeks, "", `By who: ${who}`, "", from].join("\n");
|
|
648
649
|
}
|
|
649
650
|
|
|
@@ -751,12 +752,16 @@ const CHARTER_LINES = [
|
|
|
751
752
|
export function charterText(r) {
|
|
752
753
|
const project = r.name ?? r.project;
|
|
753
754
|
if (!r.charter) {
|
|
754
|
-
return `${project} has no charter yet.
|
|
755
|
+
return `${project} has no charter yet. Set any line, or all at once:\n harbor charter "${project}" --why "…" --in "…, …" --out "…, …" --done-when "…" --calls "…"`;
|
|
755
756
|
}
|
|
756
757
|
const lines = [`Charter · ${project}`];
|
|
757
758
|
const width = Math.max(...CHARTER_LINES.map(([, label]) => label.length));
|
|
758
759
|
for (const [key, label] of CHARTER_LINES) {
|
|
759
|
-
|
|
760
|
+
// A bulleted In or Out (--in given more than once) keeps its lines lined up.
|
|
761
|
+
if (r.charter[key])
|
|
762
|
+
lines.push(
|
|
763
|
+
` ${label.padEnd(width)} ${String(r.charter[key]).replace(/\n/g, `\n ${" ".repeat(width)} `)}`,
|
|
764
|
+
);
|
|
760
765
|
}
|
|
761
766
|
if (r.outOfScope?.length) {
|
|
762
767
|
lines.push("", "Out of scope?");
|