@harborppm/cli 0.70.0 → 0.72.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/dist/app.mjs +9 -1
- package/lib/cli.mjs +77 -13
- package/lib/format.mjs +17 -3
- package/package.json +1 -1
package/dist/app.mjs
CHANGED
|
@@ -5879,6 +5879,14 @@ function neighbourStatus(data, projectId, status, step) {
|
|
|
5879
5879
|
function projectTasks(data, projectId) {
|
|
5880
5880
|
return [...tasksOf(data.tasks, projectId)].sort((a, b) => STATUS_ORDER.indexOf(a.status) - STATUS_ORDER.indexOf(b.status) || (a.number ?? 0) - (b.number ?? 0));
|
|
5881
5881
|
}
|
|
5882
|
+
/**
|
|
5883
|
+
* The project a new task goes in when none was picked: the only open one,
|
|
5884
|
+
* First voyage aside, as harbor add does. Null when there's a choice to make.
|
|
5885
|
+
*/
|
|
5886
|
+
function soleOpenProject(projects) {
|
|
5887
|
+
const open = projects.filter((p) => p.health !== "done" && !isVoyageProject(p.id));
|
|
5888
|
+
return open.length === 1 ? open[0].id : null;
|
|
5889
|
+
}
|
|
5882
5890
|
function blocked(data, task) {
|
|
5883
5891
|
return isBlocked$1(task, data.tasks);
|
|
5884
5892
|
}
|
|
@@ -12688,7 +12696,7 @@ function NewTask({ projectId, status, width, onDone, onFull }) {
|
|
|
12688
12696
|
const { store } = useDeps();
|
|
12689
12697
|
const projects = useHarbor((s) => s.projects);
|
|
12690
12698
|
const me = useHarbor((s) => s.currentUserId);
|
|
12691
|
-
const [project, setProject] = useState(projectId);
|
|
12699
|
+
const [project, setProject] = useState(() => projectId ?? soleOpenProject(projects));
|
|
12692
12700
|
const chosen = projects.find((p) => p.id === project);
|
|
12693
12701
|
if (!chosen) return /* @__PURE__ */ jsx(Box, {
|
|
12694
12702
|
width,
|
package/lib/cli.mjs
CHANGED
|
@@ -156,7 +156,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
156
156
|
[--ends DATE] [--budget N] [--from "DONE PROJECT"]
|
|
157
157
|
--from: plan it from a finished
|
|
158
158
|
project as that one really ran
|
|
159
|
-
harbor add "TITLE" --project P [--assignee me|none|NAME]
|
|
159
|
+
harbor add "TITLE" [--project P] [--assignee me|none|NAME]
|
|
160
160
|
[--due DATE|pilot] [--priority low|medium|high|urgent]
|
|
161
161
|
[--status S] [--comment TEXT] [--over-limit] [--dry-run]
|
|
162
162
|
with no TITLE: one title a line
|
|
@@ -284,6 +284,10 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
284
284
|
Changes show in Harbor's History as yours, "via Harbor CLI". These commands
|
|
285
285
|
never delete; the full-screen app can, as the website can, and asks first,
|
|
286
286
|
and so can harbor fog --sweep's x (drop it), one task at a time.
|
|
287
|
+
Practice: a new harbor starts with First voyage, a project of practice
|
|
288
|
+
tasks that tick themselves off as you try things; the brief, harbor week,
|
|
289
|
+
bridge and forecast leave it out. To look around a made-up harbor instead,
|
|
290
|
+
open harborppm.com/demo or run ssh try.harborppm.com.
|
|
287
291
|
`;
|
|
288
292
|
|
|
289
293
|
/**
|
|
@@ -442,6 +446,32 @@ const REPEAT_WORD = { weekly: "every week", biweekly: "every two weeks", monthly
|
|
|
442
446
|
|
|
443
447
|
const WEEKDAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
444
448
|
|
|
449
|
+
/** The one open project a task can go in (First voyage aside), or why --project is needed. */
|
|
450
|
+
async function onlyProject(client, harbor, say = "--project NAME") {
|
|
451
|
+
const r = await client.call("list_projects", {
|
|
452
|
+
harbor,
|
|
453
|
+
health: ["on-track", "at-risk", "off-track", "paused"],
|
|
454
|
+
limit: 5,
|
|
455
|
+
});
|
|
456
|
+
const open = (r.projects ?? []).filter((p) => p.id !== "voyage");
|
|
457
|
+
if (open.length === 1) return open[0].name;
|
|
458
|
+
if (open.length === 0)
|
|
459
|
+
throw new HarborError('This harbor has no open project yet; harbor project "NAME" --program P makes one.');
|
|
460
|
+
const names = open.slice(0, 3).map((p) => p.name);
|
|
461
|
+
const more = open.length > 3 || r.total > 5 ? ", …" : "";
|
|
462
|
+
throw new HarborError(`Say which project: ${say} (${names.join(", ")}${more}).`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const WEEKDAY_NAME = WEEKDAYS.map((d) => d[0].toUpperCase() + d.slice(1));
|
|
466
|
+
|
|
467
|
+
/** The Monday of a day's week; Saturday and Sunday look ahead to the next, as Harbor's weeks do. */
|
|
468
|
+
function mondayOf(iso) {
|
|
469
|
+
const d = new Date(`${iso}T12:00:00`);
|
|
470
|
+
const wd = d.getDay();
|
|
471
|
+
const back = wd === 0 ? -1 : wd === 6 ? -2 : wd - 1;
|
|
472
|
+
return todayLocal(new Date(d.getFullYear(), d.getMonth(), d.getDate() - back));
|
|
473
|
+
}
|
|
474
|
+
|
|
445
475
|
/** "fri", "Friday": how many days until that weekday comes round (today counts). */
|
|
446
476
|
function daysToWeekday(v, today) {
|
|
447
477
|
const w = v.toLowerCase();
|
|
@@ -621,6 +651,9 @@ const DETAIL = {
|
|
|
621
651
|
" harbor add --project P --dry-run reads: says what it would add, and",
|
|
622
652
|
" adds nothing",
|
|
623
653
|
"",
|
|
654
|
+
" --project can be left out when the harbor has one open project (First",
|
|
655
|
+
" voyage aside); the task goes there.",
|
|
656
|
+
"",
|
|
624
657
|
" --assignee me|none|NAME, --due DATE|pilot, --priority low|medium|high|urgent,",
|
|
625
658
|
" --status S, --comment TEXT and --over-limit apply to every task added, so",
|
|
626
659
|
" ten lines with --comment add the same comment to all ten. More than 100",
|
|
@@ -645,7 +678,10 @@ const DETAIL = {
|
|
|
645
678
|
" harbor week --day-hours N writes: the hours you work a day",
|
|
646
679
|
"",
|
|
647
680
|
" Only you see your week. Tasks in review are left out of what's not on a",
|
|
648
|
-
" day yet, since they wait on someone else.",
|
|
681
|
+
" day yet, since they wait on someone else. A weekday already gone this",
|
|
682
|
+
" week means next week's (mon on a Friday is the coming Monday); add",
|
|
683
|
+
" --week today to plan this week's. First voyage's practice tasks are left",
|
|
684
|
+
" out.",
|
|
649
685
|
],
|
|
650
686
|
init: [
|
|
651
687
|
" harbor init [PROJECT] [--hook] writes: .harbor/ at the top of this",
|
|
@@ -1158,11 +1194,13 @@ const DETAIL = {
|
|
|
1158
1194
|
' harbor risk "TITLE" --project P writes: logs a risk on the project',
|
|
1159
1195
|
' harbor risk "TITLE" --program P writes: logs one on the program',
|
|
1160
1196
|
" [--impact 1-5] [--probability 1-5] [--owner NAME] [--due DATE]",
|
|
1197
|
+
" With neither, it goes in the harbor's one open project (First voyage aside).",
|
|
1161
1198
|
],
|
|
1162
1199
|
issue: [
|
|
1163
1200
|
' harbor issue "TITLE" --project P writes: logs an issue on the project',
|
|
1164
1201
|
' harbor issue "TITLE" --program P writes: logs one on the program',
|
|
1165
1202
|
" [--impact 1-5] [--owner NAME] [--due DATE]",
|
|
1203
|
+
" With neither, it goes in the harbor's one open project (First voyage aside).",
|
|
1166
1204
|
],
|
|
1167
1205
|
decision: [
|
|
1168
1206
|
' harbor decision "TITLE" --project P writes: logs a decision to make',
|
|
@@ -1170,6 +1208,7 @@ const DETAIL = {
|
|
|
1170
1208
|
" [--owner NAME] [--due DATE] (the date to decide by)",
|
|
1171
1209
|
" [--turned-down T] (what was turned down, and why not)",
|
|
1172
1210
|
" [--look-again DATE] (when it comes back to the top of the brief)",
|
|
1211
|
+
" With neither, it goes in the harbor's one open project (First voyage aside).",
|
|
1173
1212
|
' harbor decision "TITLE" --supersedes "OLD"',
|
|
1174
1213
|
" writes: a decision that replaces an",
|
|
1175
1214
|
" earlier one, which reads as",
|
|
@@ -1252,6 +1291,9 @@ const OTHER_NAMES = {
|
|
|
1252
1291
|
longpole: "harbor why PROJECT shows what's holding it up.",
|
|
1253
1292
|
"long-pole": "harbor why PROJECT shows what's holding it up.",
|
|
1254
1293
|
pilot: "harbor show TASK says the date its owner's queue suggests; --due pilot on add or edit takes it.",
|
|
1294
|
+
milestone: "Milestones belong to a program: in the app (harbor), open the program and press m; or on the website.",
|
|
1295
|
+
milestones: "The app's program screen lists them: harbor, then 4, and Enter on a program.",
|
|
1296
|
+
decide: 'harbor decision "TITLE" --project P logs a decision.',
|
|
1255
1297
|
dock: "harbor try PROJECT is the dry dock.",
|
|
1256
1298
|
"dry-dock": "harbor try PROJECT is the dry dock.",
|
|
1257
1299
|
};
|
|
@@ -1809,7 +1851,7 @@ export async function run(argv, io = {}) {
|
|
|
1809
1851
|
const client = createClient({ creds, save: (c) => writeCredentials(c, env), fetcher });
|
|
1810
1852
|
// A terminal's lines fit its width; piped output stays whole for scripts.
|
|
1811
1853
|
const opts = {
|
|
1812
|
-
today: todayLocal(),
|
|
1854
|
+
today: todayLocal(io.now ? new Date(io.now()) : new Date()),
|
|
1813
1855
|
width: process.stdout.isTTY ? process.stdout.columns : undefined,
|
|
1814
1856
|
};
|
|
1815
1857
|
|
|
@@ -2298,7 +2340,8 @@ export async function run(argv, io = {}) {
|
|
|
2298
2340
|
["project", "assignee", "due", "priority", "status", "comment"],
|
|
2299
2341
|
),
|
|
2300
2342
|
);
|
|
2301
|
-
|
|
2343
|
+
// No --project: the harbor's one open project, if that's all there is.
|
|
2344
|
+
if (!values.project) values.project = await onlyProject(client, harbor);
|
|
2302
2345
|
// Several at once: one title a line on stdin, with no title (or -) after add.
|
|
2303
2346
|
const piped =
|
|
2304
2347
|
positionals.length === 0 || (positionals.length === 1 && positionals[0] === "-");
|
|
@@ -2335,7 +2378,11 @@ export async function run(argv, io = {}) {
|
|
|
2335
2378
|
dryRun: true,
|
|
2336
2379
|
project: values.project,
|
|
2337
2380
|
titles,
|
|
2338
|
-
...(pilot
|
|
2381
|
+
...(pilot
|
|
2382
|
+
? { due: pilot.map((d) => d.earliest) }
|
|
2383
|
+
: values.due
|
|
2384
|
+
? { due: titles.map(() => dueDate(values.due)) }
|
|
2385
|
+
: {}),
|
|
2339
2386
|
}),
|
|
2340
2387
|
);
|
|
2341
2388
|
else
|
|
@@ -2343,7 +2390,10 @@ export async function run(argv, io = {}) {
|
|
|
2343
2390
|
plain(
|
|
2344
2391
|
[
|
|
2345
2392
|
`Would add ${n} to ${values.project}:`,
|
|
2346
|
-
...titles.map(
|
|
2393
|
+
...titles.map(
|
|
2394
|
+
(t, i) =>
|
|
2395
|
+
` ${t}${pilot ? ` (due ${pilot[i].earliest})` : values.due ? ` (due ${dueDate(values.due)})` : ""}`,
|
|
2396
|
+
),
|
|
2347
2397
|
...(pilot
|
|
2348
2398
|
? [
|
|
2349
2399
|
`--due pilot: ${pilot[0].says}; ${pilot[0].why.map((w) => w.charAt(0).toLowerCase() + w.slice(1)).join("; ")}.`,
|
|
@@ -2536,11 +2586,14 @@ export async function run(argv, io = {}) {
|
|
|
2536
2586
|
throw new HarborError(`--${name} is a number of hours, like 2 or 1.5.`);
|
|
2537
2587
|
return n;
|
|
2538
2588
|
};
|
|
2539
|
-
// The day comes as --day; a bare second word
|
|
2589
|
+
// The day comes as --day; a bare second word (the first form) is refused, not guessed.
|
|
2540
2590
|
const [task, bareDay, ...extra] = positionals;
|
|
2541
|
-
if (extra.length
|
|
2542
|
-
throw new HarborError(
|
|
2543
|
-
|
|
2591
|
+
if (bareDay && !extra.length && !values.day)
|
|
2592
|
+
throw new HarborError(
|
|
2593
|
+
`Say the day with --day: harbor week ${task} --day ${bareDay.toLowerCase()}.`,
|
|
2594
|
+
);
|
|
2595
|
+
if (bareDay) throw new HarborError("Give a task key and a day: harbor week RR-42 --day wed.");
|
|
2596
|
+
const day = values.day;
|
|
2544
2597
|
const dayHours = number(values["day-hours"], "day-hours");
|
|
2545
2598
|
const show = (r) =>
|
|
2546
2599
|
[r.done, weekText(r, { ...opts, review: Boolean(values.review) })]
|
|
@@ -2555,18 +2608,28 @@ export async function run(argv, io = {}) {
|
|
|
2555
2608
|
throw new HarborError(
|
|
2556
2609
|
`Say which day for ${task}: harbor week ${task} --day wed, or --off to take it off the week.`,
|
|
2557
2610
|
);
|
|
2611
|
+
// A weekday already gone this week means the next one, as --due friday does.
|
|
2612
|
+
let planWeek = week;
|
|
2613
|
+
let ahead = "";
|
|
2614
|
+
if (day && !values.off && !values.week && !values.next && !values.last) {
|
|
2615
|
+
const next = dueDate(day, new Date(`${opts.today}T12:00:00`));
|
|
2616
|
+
if (next !== day && mondayOf(next) !== mondayOf(opts.today)) {
|
|
2617
|
+
planWeek = next;
|
|
2618
|
+
ahead = `${WEEKDAY_NAME[new Date(`${next}T12:00:00`).getDay()]} has gone this week, so that's next week's (${next}); add --week today for this week's.`;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2558
2621
|
return call(
|
|
2559
2622
|
"plan_week",
|
|
2560
2623
|
{
|
|
2561
2624
|
harbor,
|
|
2562
|
-
week,
|
|
2625
|
+
week: planWeek,
|
|
2563
2626
|
...(task ? { task } : {}),
|
|
2564
2627
|
...(day ? { day } : {}),
|
|
2565
2628
|
...(values.hours ? { hours: number(values.hours, "hours") } : {}),
|
|
2566
2629
|
...(values.off ? { remove: true } : {}),
|
|
2567
2630
|
...(dayHours !== undefined ? { dayHours } : {}),
|
|
2568
2631
|
},
|
|
2569
|
-
show,
|
|
2632
|
+
(r) => [ahead, show(r)].filter(Boolean).join("\n\n"),
|
|
2570
2633
|
);
|
|
2571
2634
|
}
|
|
2572
2635
|
case "comment": {
|
|
@@ -3350,8 +3413,9 @@ export async function run(argv, io = {}) {
|
|
|
3350
3413
|
),
|
|
3351
3414
|
),
|
|
3352
3415
|
);
|
|
3416
|
+
// Neither: the harbor's one open project, as harbor add does.
|
|
3353
3417
|
if (!values.project && !values.program && !values.supersedes) {
|
|
3354
|
-
|
|
3418
|
+
values.project = await onlyProject(client, harbor, "--project NAME or --program NAME");
|
|
3355
3419
|
}
|
|
3356
3420
|
return call(
|
|
3357
3421
|
"log_raid",
|
package/lib/format.mjs
CHANGED
|
@@ -31,10 +31,13 @@ export function statusWord(typed) {
|
|
|
31
31
|
* piped output keeps every title whole.
|
|
32
32
|
*/
|
|
33
33
|
export function taskLine(t, { today, mine = false, width } = {}) {
|
|
34
|
-
const
|
|
34
|
+
const open = t.status !== "done";
|
|
35
|
+
const late = t.due && today && t.due < today && open;
|
|
36
|
+
// Today and tomorrow in words, so what's close stands out from the dates.
|
|
37
|
+
const soon = open && today && t.due ? soonWord(t.due, today) : null;
|
|
35
38
|
const bits = [
|
|
36
39
|
STATUS[t.status] ?? t.status,
|
|
37
|
-
t.due ? (late ? `late, due ${t.due}` : `due ${t.due}`) : null,
|
|
40
|
+
t.due ? (late ? `late, due ${t.due}` : soon ? `due ${soon}` : `due ${t.due}`) : null,
|
|
38
41
|
t.priority && t.priority !== "medium" ? t.priority : null,
|
|
39
42
|
mine ? null : (t.assignee ?? null),
|
|
40
43
|
t.project ?? null,
|
|
@@ -45,6 +48,14 @@ export function taskLine(t, { today, mine = false, width } = {}) {
|
|
|
45
48
|
return key + fitTitle(t.title, width ? width - 1 - key.length - tail.length : Infinity) + tail;
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
/** "today" or "tomorrow" for a due date that close, else null. */
|
|
52
|
+
function soonWord(due, today) {
|
|
53
|
+
if (due === today) return "today";
|
|
54
|
+
const d = new Date(`${today}T12:00:00`);
|
|
55
|
+
d.setDate(d.getDate() + 1);
|
|
56
|
+
return due === todayLocal(d) ? "tomorrow" : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
/** The title cut to `room` characters with an ellipsis, but never below 20. */
|
|
49
60
|
function fitTitle(title, room) {
|
|
50
61
|
const chars = [...title];
|
|
@@ -1053,7 +1064,10 @@ export function weekText(w, { today, review = false, width } = {}) {
|
|
|
1053
1064
|
}
|
|
1054
1065
|
}
|
|
1055
1066
|
if (w.unplannedCount) {
|
|
1056
|
-
out.push(
|
|
1067
|
+
out.push(
|
|
1068
|
+
"",
|
|
1069
|
+
`Not on a day yet: ${w.unplannedCount}. harbor week KEY --day DAY puts one there.`,
|
|
1070
|
+
);
|
|
1057
1071
|
for (const t of w.unplanned.slice(0, 5))
|
|
1058
1072
|
out.push(` ${taskLine(t, { today, mine: true, width: width ? width - 2 : undefined })}`);
|
|
1059
1073
|
if (w.unplannedCount > 5)
|