@harborppm/cli 0.61.0 → 0.63.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 +14 -8
- package/dist/app.mjs +779 -96
- package/lib/cli.mjs +145 -12
- package/lib/format.mjs +9 -1
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -107,11 +107,16 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
107
107
|
harbor hill PROJECT --at 0-100|top|done move it: up to 50 is figuring it
|
|
108
108
|
out, past 50 is getting it done
|
|
109
109
|
harbor mine what's on you, late first
|
|
110
|
+
harbor clock [NAME] [--zone ZONE|here|none] [--hours 9-17]
|
|
111
|
+
who's working now, or set your
|
|
112
|
+
(or NAME's) time zone and hours
|
|
110
113
|
harbor tasks [words] [--project P] [--assignee me|none|NAME]
|
|
111
114
|
[--late] [--status S,S] [--all]
|
|
112
115
|
the first 20 (--all: every one);
|
|
113
116
|
done ones only with --status done
|
|
114
|
-
harbor show TASK one task (TASK: a key like RR-42)
|
|
117
|
+
harbor show TASK one task (TASK: a key like RR-42),
|
|
118
|
+
with the date its owner's queue
|
|
119
|
+
suggests
|
|
115
120
|
harbor open [TASK|PROJECT] open it on the website (or Today)
|
|
116
121
|
harbor invite EMAIL [--viewer] invite someone to your harbor
|
|
117
122
|
harbor prompt [--shell SHELL] [--refresh] due today and late, for your
|
|
@@ -145,10 +150,11 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
145
150
|
project as that one really ran
|
|
146
151
|
harbor add "TITLE" --project P [--assignee me|none|NAME]
|
|
147
152
|
[--due DATE|pilot] [--priority low|medium|high|urgent]
|
|
148
|
-
[--status S] [--comment TEXT] [--over-limit]
|
|
153
|
+
[--status S] [--comment TEXT] [--over-limit] [--dry-run]
|
|
149
154
|
with no TITLE: one title a line
|
|
150
155
|
from stdin; --due pilot takes the
|
|
151
156
|
date the owner's queue suggests
|
|
157
|
+
(harbor help add)
|
|
152
158
|
harbor start TASK | submit TASK | done TASK [--confirm] [--over-limit]
|
|
153
159
|
move a task to In play, In review
|
|
154
160
|
or Done
|
|
@@ -163,9 +169,10 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
163
169
|
your own week: tasks on days
|
|
164
170
|
against your hours, only for
|
|
165
171
|
you; --review: planned vs done
|
|
166
|
-
harbor week TASK DAY [--hours N]
|
|
167
|
-
harbor week TASK --off [DAY]
|
|
172
|
+
harbor week TASK --day DAY [--hours N] put a task on a day: mon to fri
|
|
173
|
+
harbor week TASK --off [--day DAY] take it off that day, or the week
|
|
168
174
|
harbor week --day-hours N the hours you work a day
|
|
175
|
+
(harbor help week)
|
|
169
176
|
harbor comment TASK "TEXT"
|
|
170
177
|
harbor checkin "WHAT YOU'RE DOING" [--on-blocker]
|
|
171
178
|
harbor risk "TITLE" --project P|--program P [--impact 1-5] [--probability 1-5]
|
|
@@ -555,6 +562,39 @@ export function voyageForTerminal(r, project = r.project) {
|
|
|
555
562
|
|
|
556
563
|
/** Commands whose forms do different things: one line each, saying which read and which write. */
|
|
557
564
|
const DETAIL = {
|
|
565
|
+
add: [
|
|
566
|
+
' harbor add "TITLE" --project P writes: one task, yours unless',
|
|
567
|
+
" --assignee says otherwise",
|
|
568
|
+
" harbor add --project P < titles.txt writes: one task a line of stdin",
|
|
569
|
+
" harbor add --project P --dry-run reads: says what it would add, and",
|
|
570
|
+
" adds nothing",
|
|
571
|
+
"",
|
|
572
|
+
" --assignee me|none|NAME, --due DATE|pilot, --priority low|medium|high|urgent,",
|
|
573
|
+
" --status S, --comment TEXT and --over-limit apply to every task added, so",
|
|
574
|
+
" ten lines with --comment add the same comment to all ten. More than 100",
|
|
575
|
+
" lines is harbor import's job.",
|
|
576
|
+
"",
|
|
577
|
+
" --due pilot takes the earliest day the owner's queue suggests: their open",
|
|
578
|
+
" work due first, their weekly pace, time away, what it waits on and how",
|
|
579
|
+
" long their similar tasks took. It needs an owner (--assignee none is",
|
|
580
|
+
" refused), and an empty queue gives their next working day. Several lines",
|
|
581
|
+
" each count the ones before them; with --dry-run it shows each line's date.",
|
|
582
|
+
],
|
|
583
|
+
week: [
|
|
584
|
+
" harbor week [--next|--last|--week DATE] reads: your week, the days and",
|
|
585
|
+
" their hours, and what's not on a",
|
|
586
|
+
" day yet",
|
|
587
|
+
" harbor week --review reads: what you planned against",
|
|
588
|
+
" what you finished",
|
|
589
|
+
" harbor week TASK --day DAY [--hours N] writes: puts the task on that day",
|
|
590
|
+
" (mon to fri), 2 hours unless --hours",
|
|
591
|
+
" harbor week TASK --off [--day DAY] writes: takes it off that day, or",
|
|
592
|
+
" off the week",
|
|
593
|
+
" harbor week --day-hours N writes: the hours you work a day",
|
|
594
|
+
"",
|
|
595
|
+
" Only you see your week. Tasks in review are left out of what's not on a",
|
|
596
|
+
" day yet, since they wait on someone else.",
|
|
597
|
+
],
|
|
558
598
|
init: [
|
|
559
599
|
" harbor init [PROJECT] [--hook] writes: .harbor/ at the top of this",
|
|
560
600
|
" repository, one file per task in",
|
|
@@ -829,7 +869,9 @@ const DETAIL = {
|
|
|
829
869
|
" --project P one project's changes only",
|
|
830
870
|
" --bell ring the terminal's bell when something of yours changes",
|
|
831
871
|
" --since TIME start with every change since TIME (14:30 today, or",
|
|
832
|
-
" 2026-10-21, or 2026-10-21T14:30) instead of the last 5",
|
|
872
|
+
" 2026-10-21, or 2026-10-21T14:30) instead of the last 5;",
|
|
873
|
+
" times are this computer's, or UTC with Z or UTC after",
|
|
874
|
+
" (14:30Z), and the header names the zone",
|
|
833
875
|
" --once print those and stop instead of listening",
|
|
834
876
|
" --json one JSON object a line (at, text, quote, mine, tone;",
|
|
835
877
|
" earlier: true for the ones from before it started)",
|
|
@@ -1870,6 +1912,8 @@ export async function run(argv, io = {}) {
|
|
|
1870
1912
|
),
|
|
1871
1913
|
];
|
|
1872
1914
|
if (t.repeats) lines.push(`Repeats ${REPEAT_WORD[t.repeats] ?? t.repeats}.`);
|
|
1915
|
+
if (t.pilotDue?.fits) lines.push(`${t.pilotDue.says}, so the due date holds.`);
|
|
1916
|
+
else if (t.pilotDue) lines.push(...pilotLines({ pilot: t.pilotDue }, t));
|
|
1873
1917
|
if (t.description) lines.push("", t.description);
|
|
1874
1918
|
if (t.checklist.length)
|
|
1875
1919
|
lines.push("", ...t.checklist.map((c) => ` [${c.done ? "x" : " "}] ${c.step}`));
|
|
@@ -2105,7 +2149,10 @@ export async function run(argv, io = {}) {
|
|
|
2105
2149
|
}
|
|
2106
2150
|
case "add": {
|
|
2107
2151
|
const { positionals, values, harbor } = parse(
|
|
2108
|
-
spec(
|
|
2152
|
+
spec(
|
|
2153
|
+
["over-limit", "dry-run"],
|
|
2154
|
+
["project", "assignee", "due", "priority", "status", "comment"],
|
|
2155
|
+
),
|
|
2109
2156
|
);
|
|
2110
2157
|
if (!values.project) throw new HarborError("Say which project: --project NAME.");
|
|
2111
2158
|
// Several at once: one title a line on stdin, with no title (or -) after add.
|
|
@@ -2125,6 +2172,44 @@ export async function run(argv, io = {}) {
|
|
|
2125
2172
|
throw new HarborError(
|
|
2126
2173
|
"That's more than 100 tasks; harbor import FILE takes a spreadsheet.",
|
|
2127
2174
|
);
|
|
2175
|
+
if (values["dry-run"]) {
|
|
2176
|
+
const n = titles.length === 1 ? "1 task" : `${titles.length} tasks`;
|
|
2177
|
+
// --due pilot: the date each line would take, each counting the ones before it.
|
|
2178
|
+
const pilot = duePilot(values.due)
|
|
2179
|
+
? (
|
|
2180
|
+
await client.call("suggest_due", {
|
|
2181
|
+
harbor,
|
|
2182
|
+
project: values.project,
|
|
2183
|
+
assignee: values.assignee ?? "me",
|
|
2184
|
+
count: titles.length,
|
|
2185
|
+
})
|
|
2186
|
+
).dates
|
|
2187
|
+
: null;
|
|
2188
|
+
if (asJson)
|
|
2189
|
+
out(
|
|
2190
|
+
jsonText({
|
|
2191
|
+
dryRun: true,
|
|
2192
|
+
project: values.project,
|
|
2193
|
+
titles,
|
|
2194
|
+
...(pilot ? { due: pilot.map((d) => d.earliest) } : {}),
|
|
2195
|
+
}),
|
|
2196
|
+
);
|
|
2197
|
+
else
|
|
2198
|
+
out(
|
|
2199
|
+
plain(
|
|
2200
|
+
[
|
|
2201
|
+
`Would add ${n} to ${values.project}:`,
|
|
2202
|
+
...titles.map((t, i) => ` ${t}${pilot ? ` (due ${pilot[i].earliest})` : ""}`),
|
|
2203
|
+
...(pilot
|
|
2204
|
+
? [
|
|
2205
|
+
`--due pilot: ${pilot[0].says}; ${pilot[0].why.map((w) => w.charAt(0).toLowerCase() + w.slice(1)).join("; ")}.`,
|
|
2206
|
+
]
|
|
2207
|
+
: []),
|
|
2208
|
+
].join("\n"),
|
|
2209
|
+
),
|
|
2210
|
+
);
|
|
2211
|
+
return 0;
|
|
2212
|
+
}
|
|
2128
2213
|
const added = [];
|
|
2129
2214
|
for (const title of titles) {
|
|
2130
2215
|
const r = await client
|
|
@@ -2290,7 +2375,7 @@ export async function run(argv, io = {}) {
|
|
|
2290
2375
|
}
|
|
2291
2376
|
case "week": {
|
|
2292
2377
|
const { positionals, values, harbor } = parse(
|
|
2293
|
-
spec(["next", "last", "review", "off"], ["week", "hours", "day-hours"]),
|
|
2378
|
+
spec(["next", "last", "review", "off"], ["week", "hours", "day-hours", "day"]),
|
|
2294
2379
|
);
|
|
2295
2380
|
if ([values.next, values.last, values.week].filter(Boolean).length > 1)
|
|
2296
2381
|
throw new HarborError("Say one of --next, --last or --week DATE.");
|
|
@@ -2305,21 +2390,24 @@ export async function run(argv, io = {}) {
|
|
|
2305
2390
|
throw new HarborError(`--${name} is a number of hours, like 2 or 1.5.`);
|
|
2306
2391
|
return n;
|
|
2307
2392
|
};
|
|
2308
|
-
|
|
2309
|
-
|
|
2393
|
+
// The day comes as --day; a bare second word still works, as it did first.
|
|
2394
|
+
const [task, bareDay, ...extra] = positionals;
|
|
2395
|
+
if (extra.length || (bareDay && values.day))
|
|
2396
|
+
throw new HarborError("Give a task key and a day: harbor week RR-42 --day wed.");
|
|
2397
|
+
const day = values.day ?? bareDay;
|
|
2310
2398
|
const dayHours = number(values["day-hours"], "day-hours");
|
|
2311
2399
|
const show = (r) =>
|
|
2312
2400
|
[r.done, weekText(r, { ...opts, review: Boolean(values.review) })]
|
|
2313
2401
|
.filter(Boolean)
|
|
2314
2402
|
.join("\n\n");
|
|
2315
2403
|
if (!task && dayHours === undefined) {
|
|
2316
|
-
if (values.off || values.hours)
|
|
2317
|
-
throw new HarborError("Say which task: harbor week RR-42 wed --hours 3.");
|
|
2404
|
+
if (values.off || values.hours || values.day)
|
|
2405
|
+
throw new HarborError("Say which task: harbor week RR-42 --day wed --hours 3.");
|
|
2318
2406
|
return call("get_week", { harbor, week }, show);
|
|
2319
2407
|
}
|
|
2320
2408
|
if (task && !day && !values.off)
|
|
2321
2409
|
throw new HarborError(
|
|
2322
|
-
`Say which day for ${task}: harbor week ${task} wed, or --off to take it off the week.`,
|
|
2410
|
+
`Say which day for ${task}: harbor week ${task} --day wed, or --off to take it off the week.`,
|
|
2323
2411
|
);
|
|
2324
2412
|
return call(
|
|
2325
2413
|
"plan_week",
|
|
@@ -2346,6 +2434,51 @@ export async function run(argv, io = {}) {
|
|
|
2346
2434
|
`Comment added to ${r.commented.task}${r.mentioned?.length ? `; told ${r.mentioned.join(", ")}` : ""}.`,
|
|
2347
2435
|
);
|
|
2348
2436
|
}
|
|
2437
|
+
case "clock": {
|
|
2438
|
+
const { positionals, values, harbor } = parse(spec([], ["zone", "hours"]));
|
|
2439
|
+
const who = positionals.join(" ").trim();
|
|
2440
|
+
if (values.zone === undefined && values.hours === undefined) {
|
|
2441
|
+
return call("crew_clocks", { harbor }, (r) => {
|
|
2442
|
+
const rows = who ? r.clocks.filter((c) => nameHas(c.who, who)) : r.clocks;
|
|
2443
|
+
const quiet = who ? r.noTimeZone.filter((n) => nameHas(n, who)) : r.noTimeZone;
|
|
2444
|
+
if (rows.length === 0 && quiet.length === 0) {
|
|
2445
|
+
return who ? `Nobody on the crew matches "${who}".` : "Nobody on the crew yet.";
|
|
2446
|
+
}
|
|
2447
|
+
const width = Math.max(0, ...rows.map((c) => c.who.length));
|
|
2448
|
+
return [
|
|
2449
|
+
...rows.map(
|
|
2450
|
+
(c) =>
|
|
2451
|
+
`${c.who.padEnd(width)} ${c.localTime.padStart(5)} ${c.zone.replace(/_/g, " ")} ${c.working ? `working (${c.hours})` : (c.picksUp ?? "off")}`,
|
|
2452
|
+
),
|
|
2453
|
+
...(quiet.length
|
|
2454
|
+
? [
|
|
2455
|
+
`${rows.length ? "\n" : ""}No time zone yet: ${quiet.join(", ")}. harbor clock --zone here sets yours.`,
|
|
2456
|
+
]
|
|
2457
|
+
: []),
|
|
2458
|
+
].join("\n");
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
let zone = values.zone;
|
|
2462
|
+
if (zone?.toLowerCase() === "here") {
|
|
2463
|
+
zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2464
|
+
if (!zone) throw new HarborError("This machine doesn't say its time zone. Give one, like --zone Europe/London.");
|
|
2465
|
+
}
|
|
2466
|
+
let starts;
|
|
2467
|
+
let ends;
|
|
2468
|
+
if (values.hours !== undefined) {
|
|
2469
|
+
const m = /^\s*([^-–]+?)\s*[-–]\s*(.+?)\s*$/.exec(values.hours);
|
|
2470
|
+
if (!m) throw new HarborError("--hours is a start and an end, like 9-17 or 8:30-16:30.");
|
|
2471
|
+
[, starts, ends] = m;
|
|
2472
|
+
}
|
|
2473
|
+
return call(
|
|
2474
|
+
"set_clock",
|
|
2475
|
+
{ harbor, person: who || "me", zone, starts, ends },
|
|
2476
|
+
(r) =>
|
|
2477
|
+
r.zone
|
|
2478
|
+
? `${r.person}: ${r.zone.replace(/_/g, " ")}, ${r.hours} on weekdays. ${r.working ? `Working now (${r.localTime} there).` : (r.picksUp ?? "")}`.trim()
|
|
2479
|
+
: `${r.person} has no time zone now.`,
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2349
2482
|
case "checkin": {
|
|
2350
2483
|
const { positionals, values, harbor } = parse(spec(["on-blocker"]));
|
|
2351
2484
|
return call(
|
package/lib/format.mjs
CHANGED
|
@@ -1029,8 +1029,16 @@ export function weekText(w, { today, review = false, width } = {}) {
|
|
|
1029
1029
|
out.push("", `Not on a day yet: ${w.unplannedCount}. harbor week KEY DAY puts one there.`);
|
|
1030
1030
|
for (const t of w.unplanned.slice(0, 5))
|
|
1031
1031
|
out.push(` ${taskLine(t, { today, mine: true, width: width ? width - 2 : undefined })}`);
|
|
1032
|
-
if (w.unplannedCount > 5)
|
|
1032
|
+
if (w.unplannedCount > 5)
|
|
1033
|
+
out.push(
|
|
1034
|
+
` … and ${w.unplannedCount - 5} more (--json lists ${w.unplannedCount > w.unplanned.length ? w.unplanned.length : "all"})`,
|
|
1035
|
+
);
|
|
1033
1036
|
}
|
|
1037
|
+
if (w.inReviewCount)
|
|
1038
|
+
out.push(
|
|
1039
|
+
"",
|
|
1040
|
+
`${w.inReviewCount} of yours ${w.inReviewCount === 1 ? "is" : "are"} in review, waiting on someone else, so ${w.inReviewCount === 1 ? "it's" : "they're"} left out.`,
|
|
1041
|
+
);
|
|
1034
1042
|
}
|
|
1035
1043
|
if (reviewing) {
|
|
1036
1044
|
const r = w.review;
|