@harborppm/cli 0.71.0 → 0.73.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 +13 -5
- package/dist/app.mjs +771 -916
- package/lib/cli.mjs +81 -12
- package/lib/format.mjs +3 -1
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -112,7 +112,8 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
112
112
|
harbor hill [--program P] where each project is on its hill
|
|
113
113
|
harbor hill PROJECT --at 0-100|top|done move it: up to 50 is figuring it
|
|
114
114
|
out, past 50 is getting it done
|
|
115
|
-
harbor mine what's on you, late first
|
|
115
|
+
harbor mine what's on you, late first, and
|
|
116
|
+
what you're also on
|
|
116
117
|
harbor catchup [--since DATE] while you were away: handed to
|
|
117
118
|
you, decided, what moved
|
|
118
119
|
harbor clock [NAME] [--zone ZONE|here|none] [--hours 9-17]
|
|
@@ -123,8 +124,11 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
123
124
|
the first 20 (--all: every one);
|
|
124
125
|
done ones only with --status done
|
|
125
126
|
harbor show TASK one task (TASK: a key like RR-42),
|
|
126
|
-
|
|
127
|
-
suggests
|
|
127
|
+
who's on it, its files, and the
|
|
128
|
+
date its owner's queue suggests
|
|
129
|
+
harbor files PROJECT [--kind image|doc|link]
|
|
130
|
+
every file on a project's tasks,
|
|
131
|
+
newest first
|
|
128
132
|
harbor open [TASK|PROJECT] open it on the website (or Today)
|
|
129
133
|
harbor invite EMAIL [--viewer] invite someone to your harbor
|
|
130
134
|
harbor prompt [--shell SHELL] [--refresh] due today and late, for your
|
|
@@ -157,7 +161,8 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
157
161
|
--from: plan it from a finished
|
|
158
162
|
project as that one really ran
|
|
159
163
|
harbor add "TITLE" [--project P] [--assignee me|none|NAME]
|
|
160
|
-
[--
|
|
164
|
+
[--also "NAME, NAME"] [--due DATE|pilot]
|
|
165
|
+
[--priority low|medium|high|urgent]
|
|
161
166
|
[--status S] [--comment TEXT] [--over-limit] [--dry-run]
|
|
162
167
|
with no TITLE: one title a line
|
|
163
168
|
from stdin; --due pilot takes the
|
|
@@ -167,7 +172,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
167
172
|
move a task to In play, In review
|
|
168
173
|
or Done
|
|
169
174
|
harbor edit TASK [--title T] [--assignee me|none|NAME]
|
|
170
|
-
[--due DATE|pilot|none]
|
|
175
|
+
[--also "NAME, NAME"|none] [--due DATE|pilot|none]
|
|
171
176
|
[--priority P] [--status S] [--notes TEXT]
|
|
172
177
|
[--repeat weekly|biweekly|monthly|never]
|
|
173
178
|
[--waits-on TASK,TASK|none] [--confirm] [--over-limit]
|
|
@@ -447,7 +452,7 @@ const REPEAT_WORD = { weekly: "every week", biweekly: "every two weeks", monthly
|
|
|
447
452
|
const WEEKDAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
448
453
|
|
|
449
454
|
/** The one open project a task can go in (First voyage aside), or why --project is needed. */
|
|
450
|
-
async function onlyProject(client, harbor) {
|
|
455
|
+
async function onlyProject(client, harbor, say = "--project NAME") {
|
|
451
456
|
const r = await client.call("list_projects", {
|
|
452
457
|
harbor,
|
|
453
458
|
health: ["on-track", "at-risk", "off-track", "paused"],
|
|
@@ -459,7 +464,7 @@ async function onlyProject(client, harbor) {
|
|
|
459
464
|
throw new HarborError('This harbor has no open project yet; harbor project "NAME" --program P makes one.');
|
|
460
465
|
const names = open.slice(0, 3).map((p) => p.name);
|
|
461
466
|
const more = open.length > 3 || r.total > 5 ? ", …" : "";
|
|
462
|
-
throw new HarborError(`Say which project:
|
|
467
|
+
throw new HarborError(`Say which project: ${say} (${names.join(", ")}${more}).`);
|
|
463
468
|
}
|
|
464
469
|
|
|
465
470
|
const WEEKDAY_NAME = WEEKDAYS.map((d) => d[0].toUpperCase() + d.slice(1));
|
|
@@ -608,7 +613,7 @@ export function hint(message, project) {
|
|
|
608
613
|
)
|
|
609
614
|
.replace(
|
|
610
615
|
/^Nothing to change: pass a field to update\.$/,
|
|
611
|
-
"Nothing to change. Say what with --title, --assignee, --due, --priority, --status, --notes, --repeat or --waits-on.",
|
|
616
|
+
"Nothing to change. Say what with --title, --assignee, --also, --due, --priority, --status, --notes, --repeat or --waits-on.",
|
|
612
617
|
)
|
|
613
618
|
.replace(
|
|
614
619
|
/^This connection was allowed to read only\..*$/s,
|
|
@@ -642,6 +647,24 @@ export function voyageForTerminal(r, project = r.project) {
|
|
|
642
647
|
};
|
|
643
648
|
}
|
|
644
649
|
|
|
650
|
+
/** --also "Sam, Aya" as the names; none (or nobody) leaves only the owner. */
|
|
651
|
+
export function alsoList(value) {
|
|
652
|
+
const v = value.trim();
|
|
653
|
+
if (/^(none|nobody)$/i.test(v)) return [];
|
|
654
|
+
const names = v
|
|
655
|
+
.split(",")
|
|
656
|
+
.map((x) => x.trim())
|
|
657
|
+
.filter(Boolean);
|
|
658
|
+
if (!names.length) throw new HarborError('Say who: --also "NAME, NAME", or --also none.');
|
|
659
|
+
return names;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** One file: its name and kind, a link's address, who added it and when. */
|
|
663
|
+
export function fileText(f) {
|
|
664
|
+
const kind = { image: "image", doc: "document", link: "link" }[f.kind] ?? f.kind;
|
|
665
|
+
return `${f.name} (${kind}${f.url ? `, ${f.url}` : ""}) · ${f.addedBy ?? "someone"}, ${f.added}`;
|
|
666
|
+
}
|
|
667
|
+
|
|
645
668
|
/** Commands whose forms do different things: one line each, saying which read and which write. */
|
|
646
669
|
const DETAIL = {
|
|
647
670
|
add: [
|
|
@@ -654,7 +677,8 @@ const DETAIL = {
|
|
|
654
677
|
" --project can be left out when the harbor has one open project (First",
|
|
655
678
|
" voyage aside); the task goes there.",
|
|
656
679
|
"",
|
|
657
|
-
" --assignee me|none|NAME, --
|
|
680
|
+
" --assignee me|none|NAME, --also \"NAME, NAME\" (others on it with the",
|
|
681
|
+
" owner), --due DATE|pilot, --priority low|medium|high|urgent,",
|
|
658
682
|
" --status S, --comment TEXT and --over-limit apply to every task added, so",
|
|
659
683
|
" ten lines with --comment add the same comment to all ten. More than 100",
|
|
660
684
|
" lines is harbor import's job.",
|
|
@@ -1194,11 +1218,13 @@ const DETAIL = {
|
|
|
1194
1218
|
' harbor risk "TITLE" --project P writes: logs a risk on the project',
|
|
1195
1219
|
' harbor risk "TITLE" --program P writes: logs one on the program',
|
|
1196
1220
|
" [--impact 1-5] [--probability 1-5] [--owner NAME] [--due DATE]",
|
|
1221
|
+
" With neither, it goes in the harbor's one open project (First voyage aside).",
|
|
1197
1222
|
],
|
|
1198
1223
|
issue: [
|
|
1199
1224
|
' harbor issue "TITLE" --project P writes: logs an issue on the project',
|
|
1200
1225
|
' harbor issue "TITLE" --program P writes: logs one on the program',
|
|
1201
1226
|
" [--impact 1-5] [--owner NAME] [--due DATE]",
|
|
1227
|
+
" With neither, it goes in the harbor's one open project (First voyage aside).",
|
|
1202
1228
|
],
|
|
1203
1229
|
decision: [
|
|
1204
1230
|
' harbor decision "TITLE" --project P writes: logs a decision to make',
|
|
@@ -1206,6 +1232,7 @@ const DETAIL = {
|
|
|
1206
1232
|
" [--owner NAME] [--due DATE] (the date to decide by)",
|
|
1207
1233
|
" [--turned-down T] (what was turned down, and why not)",
|
|
1208
1234
|
" [--look-again DATE] (when it comes back to the top of the brief)",
|
|
1235
|
+
" With neither, it goes in the harbor's one open project (First voyage aside).",
|
|
1209
1236
|
' harbor decision "TITLE" --supersedes "OLD"',
|
|
1210
1237
|
" writes: a decision that replaces an",
|
|
1211
1238
|
" earlier one, which reads as",
|
|
@@ -1288,6 +1315,9 @@ const OTHER_NAMES = {
|
|
|
1288
1315
|
longpole: "harbor why PROJECT shows what's holding it up.",
|
|
1289
1316
|
"long-pole": "harbor why PROJECT shows what's holding it up.",
|
|
1290
1317
|
pilot: "harbor show TASK says the date its owner's queue suggests; --due pilot on add or edit takes it.",
|
|
1318
|
+
milestone: "Milestones belong to a program: in the app (harbor), open the program and press m; or on the website.",
|
|
1319
|
+
milestones: "The app's program screen lists them: harbor, then 4, and Enter on a program.",
|
|
1320
|
+
decide: 'harbor decision "TITLE" --project P logs a decision.',
|
|
1291
1321
|
dock: "harbor try PROJECT is the dry dock.",
|
|
1292
1322
|
"dry-dock": "harbor try PROJECT is the dry dock.",
|
|
1293
1323
|
};
|
|
@@ -2082,6 +2112,25 @@ export async function run(argv, io = {}) {
|
|
|
2082
2112
|
out(asJson ? jsonText(result) : plain(taskList(result, opts)));
|
|
2083
2113
|
return 0;
|
|
2084
2114
|
}
|
|
2115
|
+
case "files": {
|
|
2116
|
+
const { positionals, values, harbor } = parse(spec([], ["kind"]));
|
|
2117
|
+
const project = text(positionals, "the project");
|
|
2118
|
+
const kind = values.kind?.trim().toLowerCase().replace(/s$/, "");
|
|
2119
|
+
if (kind !== undefined && !["image", "doc", "link"].includes(kind))
|
|
2120
|
+
throw new HarborError("--kind is image, doc or link.");
|
|
2121
|
+
return call("list_files", { harbor, project, kind, limit: 200 }, (r) =>
|
|
2122
|
+
r.files.length
|
|
2123
|
+
? [
|
|
2124
|
+
`${r.project}: ${r.total} file${r.total === 1 ? "" : "s"}${kind ? ` (${kind}s)` : ""}`,
|
|
2125
|
+
"",
|
|
2126
|
+
...r.files.map(
|
|
2127
|
+
(f) => `${fileText(f)} on ${f.task.key ?? f.task.id} ${f.task.title}`,
|
|
2128
|
+
),
|
|
2129
|
+
...(r.total > r.files.length ? [`…and ${r.total - r.files.length} more.`] : []),
|
|
2130
|
+
].join("\n")
|
|
2131
|
+
: `No ${kind ? `${kind}s` : "files"} on ${r.project}'s tasks yet. Attach them to a task on the website.`,
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2085
2134
|
case "show": {
|
|
2086
2135
|
const { positionals, harbor } = parse(spec());
|
|
2087
2136
|
return call("get_task", { harbor, task: one(positionals, "task key, like RR-42") }, (t) => {
|
|
@@ -2091,6 +2140,7 @@ export async function run(argv, io = {}) {
|
|
|
2091
2140
|
{ ...opts, width: undefined },
|
|
2092
2141
|
),
|
|
2093
2142
|
];
|
|
2143
|
+
if (t.alsoOn?.length) lines.push(`Also on it: ${t.alsoOn.join(", ")}.`);
|
|
2094
2144
|
if (t.repeats) lines.push(`Repeats ${REPEAT_WORD[t.repeats] ?? t.repeats}.`);
|
|
2095
2145
|
if (t.pilotDue?.fits) lines.push(`${t.pilotDue.says}, so the due date holds.`);
|
|
2096
2146
|
else if (t.pilotDue) lines.push(...pilotLines({ pilot: t.pilotDue }, t));
|
|
@@ -2113,6 +2163,7 @@ export async function run(argv, io = {}) {
|
|
|
2113
2163
|
(w) => ` ${w.key ?? w.id} ${w.title} (${STATUS[w.status] ?? w.status})`,
|
|
2114
2164
|
),
|
|
2115
2165
|
);
|
|
2166
|
+
if (t.files?.length) lines.push("", "Files:", ...t.files.map((f) => ` ${fileText(f)}`));
|
|
2116
2167
|
if (t.comments.length)
|
|
2117
2168
|
lines.push(
|
|
2118
2169
|
"",
|
|
@@ -2331,7 +2382,7 @@ export async function run(argv, io = {}) {
|
|
|
2331
2382
|
const { positionals, values, harbor } = parse(
|
|
2332
2383
|
spec(
|
|
2333
2384
|
["over-limit", "dry-run"],
|
|
2334
|
-
["project", "assignee", "due", "priority", "status", "comment"],
|
|
2385
|
+
["project", "assignee", "also", "due", "priority", "status", "comment"],
|
|
2335
2386
|
),
|
|
2336
2387
|
);
|
|
2337
2388
|
// No --project: the harbor's one open project, if that's all there is.
|
|
@@ -2407,6 +2458,7 @@ export async function run(argv, io = {}) {
|
|
|
2407
2458
|
title,
|
|
2408
2459
|
// Yours unless you say otherwise, as with n in the app.
|
|
2409
2460
|
assignee: values.assignee ?? "me",
|
|
2461
|
+
also: values.also === undefined ? undefined : alsoList(values.also),
|
|
2410
2462
|
due: duePilot(values.due) ? undefined : dueDate(values.due),
|
|
2411
2463
|
suggestedDue: duePilot(values.due),
|
|
2412
2464
|
priority: values.priority,
|
|
@@ -2520,7 +2572,17 @@ export async function run(argv, io = {}) {
|
|
|
2520
2572
|
const { positionals, values, harbor } = parse(
|
|
2521
2573
|
spec(
|
|
2522
2574
|
["confirm", "over-limit"],
|
|
2523
|
-
[
|
|
2575
|
+
[
|
|
2576
|
+
"title",
|
|
2577
|
+
"assignee",
|
|
2578
|
+
"also",
|
|
2579
|
+
"due",
|
|
2580
|
+
"priority",
|
|
2581
|
+
"status",
|
|
2582
|
+
"notes",
|
|
2583
|
+
"repeat",
|
|
2584
|
+
"waits-on",
|
|
2585
|
+
],
|
|
2524
2586
|
),
|
|
2525
2587
|
);
|
|
2526
2588
|
const waits = values["waits-on"];
|
|
@@ -2531,6 +2593,7 @@ export async function run(argv, io = {}) {
|
|
|
2531
2593
|
task: one(positionals, "task key, like RR-42"),
|
|
2532
2594
|
title: values.title,
|
|
2533
2595
|
assignee: values.assignee,
|
|
2596
|
+
also: values.also === undefined ? undefined : alsoList(values.also),
|
|
2534
2597
|
due:
|
|
2535
2598
|
values.due === undefined || duePilot(values.due)
|
|
2536
2599
|
? undefined
|
|
@@ -2555,6 +2618,11 @@ export async function run(argv, io = {}) {
|
|
|
2555
2618
|
(r) =>
|
|
2556
2619
|
[
|
|
2557
2620
|
`Updated: ${taskLine(r.updated, opts)}`,
|
|
2621
|
+
values.also !== undefined
|
|
2622
|
+
? r.updated.alsoOn?.length
|
|
2623
|
+
? `Also on it: ${r.updated.alsoOn.join(", ")}.`
|
|
2624
|
+
: "Only the owner is on it now."
|
|
2625
|
+
: null,
|
|
2558
2626
|
r.updated.waitsOn ? `Waits on ${r.updated.waitsOn.join(", ")}.` : null,
|
|
2559
2627
|
r.updated.repeat ? `Repeats ${REPEAT_WORD[r.updated.repeat]}.` : null,
|
|
2560
2628
|
...(values.due && !duePilot(values.due) ? pilotLines(r, r.updated) : []),
|
|
@@ -3407,8 +3475,9 @@ export async function run(argv, io = {}) {
|
|
|
3407
3475
|
),
|
|
3408
3476
|
),
|
|
3409
3477
|
);
|
|
3478
|
+
// Neither: the harbor's one open project, as harbor add does.
|
|
3410
3479
|
if (!values.project && !values.program && !values.supersedes) {
|
|
3411
|
-
|
|
3480
|
+
values.project = await onlyProject(client, harbor, "--project NAME or --program NAME");
|
|
3412
3481
|
}
|
|
3413
3482
|
return call(
|
|
3414
3483
|
"log_raid",
|
package/lib/format.mjs
CHANGED
|
@@ -39,7 +39,7 @@ export function taskLine(t, { today, mine = false, width } = {}) {
|
|
|
39
39
|
STATUS[t.status] ?? t.status,
|
|
40
40
|
t.due ? (late ? `late, due ${t.due}` : soon ? `due ${soon}` : `due ${t.due}`) : null,
|
|
41
41
|
t.priority && t.priority !== "medium" ? t.priority : null,
|
|
42
|
-
mine ? null :
|
|
42
|
+
mine ? null : t.assignee ? `${t.assignee}${t.alsoOn?.length ? ` +${t.alsoOn.length}` : ""}` : null,
|
|
43
43
|
t.project ?? null,
|
|
44
44
|
t.waitingOn ? `waits on ${t.waitingOn}` : null,
|
|
45
45
|
].filter(Boolean);
|
|
@@ -72,6 +72,8 @@ export function myWork(w, opts) {
|
|
|
72
72
|
if (!w.you) return w.note ?? "Nothing is yours in this harbor yet.";
|
|
73
73
|
const out = [`${w.you}: ${w.openTasks} open${w.late ? `, ${w.late} late` : ""}`];
|
|
74
74
|
if (w.tasks?.length) out.push("", ...w.tasks.map((t) => taskLine(t, { ...opts, mine: true })));
|
|
75
|
+
if (w.alsoOn?.length)
|
|
76
|
+
out.push("", "Also on (others own these):", ...w.alsoOn.map((t) => ` ${taskLine(t, opts)}`));
|
|
75
77
|
if (w.waitingOnYou?.length) {
|
|
76
78
|
out.push("", "Waiting on you:");
|
|
77
79
|
for (const x of w.waitingOnYou)
|