@harborppm/cli 0.61.0 → 0.62.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 +4 -4
- package/dist/app.mjs +303 -37
- package/lib/cli.mjs +57 -10
- package/lib/format.mjs +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ harbor # name your harbor and its first project, and you're in
|
|
|
28
28
|
|
|
29
29
|
1. **Today**: today's focus (`f` adds or takes off the task you're on, up to three) and your check-in (`c`); what's on you, late first; who's waiting on you; your risks and issues.
|
|
30
30
|
2. **Inbox**: the same pings as the website: waiting on you, sign-offs, mentions. Enter opens one; it's marked read.
|
|
31
|
-
3. **Projects**: every portfolio, program and project with its health. A project opens on its board (columns side by side; `f` and `p` show only someone's cards or one priority), its list, or its budget and spend ledger (`v`); `e` changes its details: lead, dates, budget, health by hand with a note. `P` runs its pre-mortem: everyone answers "it failed; what happened?" alone for five minutes, then each answer becomes a risk (`r`), folds into one already raised (`m`) or is left out (`l`).
|
|
31
|
+
3. **Projects**: every portfolio, program and project with its health. A project opens on its board (columns side by side; `f` and `p` show only someone's cards or one priority), its list, or its budget and spend ledger (`v`); space picks the card or row you're on, and `m` moves, assigns, dates, pushes a week, sets the priority of or finishes everything picked at once (`u` puts them all back); `e` changes its details: lead, dates, budget, health by hand with a note. `P` runs its pre-mortem: everyone answers "it failed; what happened?" alone for five minutes, then each answer becomes a risk (`r`), folds into one already raised (`m`) or is left out (`l`).
|
|
32
32
|
4. **Programs**: the review: programs worst first, with their lead, end date, late work and where they sit on the priority matrix; `m` writes a next move, `h` sets a health by hand (with why) or hands it back to the late work, `o` and `p` show someone's or one portfolio's. A program shows its projects and milestones side by side (`m` adds a milestone, space ticks one), its next move and health; `v` reads it project by project, each with its bar across the months, open tasks by status and worst risks. A portfolio opens as a board (programs, with risks, next milestones and money beside them); `v` reads it program by program. `e` on a program also names what's holding it back, what it waits on, its impact and its baseline, with its health history below, and `r` reads its status report. A milestone can wait on projects and need someone's sign-off (they approve it or ask for changes). A project's details set its plan by month and what it waits on; a portfolio's set the priority matrix's rules. `:` then "new portfolio" makes one.
|
|
33
33
|
5. **Risks**: the register, worst first; `t`, `o` and `p` narrow it by kind, owner or portfolio, `a` adds closed ones, `h` draws the heat map over it and `c` narrows it to one of its cells. Enter changes one with every field the website has (status, owner, likelihood, impact, mitigation, contingency, resolution, rationale, notes, updates); `n` logs a risk, issue or decision.
|
|
34
34
|
6. **People**: open and late work and the next four weeks against each person's capacity; Enter changes their capacity, role, or the time they're promised to a program.
|
|
@@ -201,7 +201,7 @@ harbor soundings [PROJECT]
|
|
|
201
201
|
harbor ashore [--weeks N] [--country CODES|none] [--person NAME]
|
|
202
202
|
harbor limit PROJECT [COLUMN N|none]
|
|
203
203
|
harbor week [--next|--last|--week DATE] [--review]
|
|
204
|
-
harbor week TASK DAY [--hours N] | week TASK --off [DAY] | week --day-hours N
|
|
204
|
+
harbor week TASK --day DAY [--hours N] | week TASK --off [--day DAY] | week --day-hours N
|
|
205
205
|
harbor init [PROJECT] [--hook] [--dry-run] a project's tasks as files in this repository
|
|
206
206
|
harbor init --local [NAME] a plan in this folder alone, no account
|
|
207
207
|
harbor sync [--quiet] [--dry-run] the files and the website, in step both ways
|
|
@@ -229,9 +229,9 @@ harbor agent report TASK "WHAT YOU DID" [--start] [--agent NAME]
|
|
|
229
229
|
|
|
230
230
|
Agents on the crew: put an AI app on People with Add an agent and assign it tasks like anyone else. An agent working through the CLI (Claude Code, say) runs `harbor agent` for its tasks, in progress first, with their notes, checklists and latest comments. When it's done, `harbor agent report RR-4 "Drafted 12 questions; two need Dana"` adds its note as the agent's comment and moves the task to In review for a person to check. An agent never marks its own work Done.
|
|
231
231
|
|
|
232
|
-
`harbor week` is your own week, and only you see it: Monday to Friday with the tasks you put on each day, the hours each should take against the hours you work (`--day-hours 8`; 7 unless you say), a warning on a day that's too full, and your open tasks not on a day yet. `harbor week RR-42 wed --hours 3` puts a task on Wednesday (`--next` for next week), `harbor week RR-42 --off` takes it off the week, and from Friday (or with `--review`) it says what you planned against what you finished. The website's Your week is the same plan, with drag and drop.
|
|
232
|
+
`harbor week` is your own week, and only you see it: Monday to Friday with the tasks you put on each day, the hours each should take against the hours you work (`--day-hours 8`; 7 unless you say), a warning on a day that's too full, and your open tasks not on a day yet (ones in review wait on someone else, so they're left out). `harbor week RR-42 --day wed --hours 3` puts a task on Wednesday (`--next` for next week), `harbor week RR-42 --off` takes it off the week, and from Friday (or with `--review`) it says what you planned against what you finished. The website's Your week is the same plan, with drag and drop.
|
|
233
233
|
|
|
234
|
-
`harbor add --project Trail --comment "From Monday's call" < list.txt` adds one task per line of the file, each with the comment.
|
|
234
|
+
`harbor add --project Trail --comment "From Monday's call" < list.txt` adds one task per line of the file, each with the comment (every option applies to every line); `--dry-run` says what it would add first.
|
|
235
235
|
|
|
236
236
|
`harbor limit RR` prints how many tasks each of a board's columns takes at once; `harbor limit RR doing 8` changes one (1 to 20, or `none` for no limit), if you own the harbor. `harbor projects --json` carries each board's `limits` too.
|
|
237
237
|
|
package/dist/app.mjs
CHANGED
|
@@ -3278,31 +3278,31 @@ function barnaclesHeadline(b) {
|
|
|
3278
3278
|
if (b.added === 0) return null;
|
|
3279
3279
|
const when = b.from === "baseline" ? "since the baseline" : "since its first week";
|
|
3280
3280
|
const pct = b.share != null ? `, ${Math.round(b.share * 100)}% more than planned` : "";
|
|
3281
|
-
return `+${plural$
|
|
3281
|
+
return `+${plural$6(b.added, "task")} ${when}${pct}`;
|
|
3282
3282
|
}
|
|
3283
3283
|
/** What the added work costs in days, and how much of the slip it explains. */
|
|
3284
3284
|
function barnaclesDaysLine(b) {
|
|
3285
3285
|
if (b.added === 0 || b.days == null || b.perWeek == null) return null;
|
|
3286
|
-
const work = `At the crew's pace (about ${b.perWeek} a week) that's about ${plural$
|
|
3286
|
+
const work = `At the crew's pace (about ${b.perWeek} a week) that's about ${plural$6(b.days, "day")} of work`;
|
|
3287
3287
|
if (b.slip == null || b.slip <= 0) return `${work}.`;
|
|
3288
|
-
if (b.days >= b.slip) return `${work}: enough to explain all ${plural$
|
|
3289
|
-
return `${work}: about ${b.days} of the ${plural$
|
|
3288
|
+
if (b.days >= b.slip) return `${work}: enough to explain all ${plural$6(b.slip, "day")} of the slip.`;
|
|
3289
|
+
return `${work}: about ${b.days} of the ${plural$6(b.slip, "day")} it has slipped.`;
|
|
3290
3290
|
}
|
|
3291
3291
|
/** One line for the brief or a list: "+14 tasks (38%) since the baseline, 3 this week, about 65 days of work". */
|
|
3292
3292
|
function barnaclesShort(b) {
|
|
3293
3293
|
const pct = b.share != null ? ` (${Math.round(b.share * 100)}%)` : "";
|
|
3294
3294
|
const when = b.from === "baseline" ? "since the baseline" : "since its first week";
|
|
3295
3295
|
const week = b.thisWeek ? `, ${b.thisWeek} this week` : "";
|
|
3296
|
-
const days = b.days != null ? `, about ${plural$
|
|
3296
|
+
const days = b.days != null ? `, about ${plural$6(b.days, "day")} of work` : "";
|
|
3297
3297
|
const top = b.adders.find((a) => a.personId);
|
|
3298
3298
|
const who = top && top.added * 2 > b.added ? `, mostly from ${top.name}` : "";
|
|
3299
|
-
return `+${plural$
|
|
3299
|
+
return `+${plural$6(b.added, "task")}${pct} ${when}${week}${days}${who}`;
|
|
3300
3300
|
}
|
|
3301
3301
|
/** Whether the brief names it: measured from a baseline, enough added, and enough of the plan. */
|
|
3302
3302
|
function briefWorthy(b) {
|
|
3303
3303
|
return b.from === "baseline" && b.added >= 3 && (b.share == null || b.share >= .15);
|
|
3304
3304
|
}
|
|
3305
|
-
var SETTLE_DAYS, BRIEF_MIN_ADDED, BRIEF_MIN_SHARE, noon$2, isoDay$3, plural$
|
|
3305
|
+
var SETTLE_DAYS, BRIEF_MIN_ADDED, BRIEF_MIN_SHARE, noon$2, isoDay$3, plural$6;
|
|
3306
3306
|
var init_barnacles = __esmMin((() => {
|
|
3307
3307
|
init_date_fns();
|
|
3308
3308
|
init_format();
|
|
@@ -3312,7 +3312,7 @@ var init_barnacles = __esmMin((() => {
|
|
|
3312
3312
|
BRIEF_MIN_SHARE = .15;
|
|
3313
3313
|
noon$2 = (iso) => /* @__PURE__ */ new Date(`${iso.slice(0, 10)}T12:00:00`);
|
|
3314
3314
|
isoDay$3 = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
3315
|
-
plural$
|
|
3315
|
+
plural$6 = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
3316
3316
|
}));
|
|
3317
3317
|
|
|
3318
3318
|
//#endregion
|
|
@@ -3791,7 +3791,7 @@ function voyageLog(data, projectId) {
|
|
|
3791
3791
|
thanks
|
|
3792
3792
|
};
|
|
3793
3793
|
}
|
|
3794
|
-
const plural$
|
|
3794
|
+
const plural$5 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
3795
3795
|
const md$2 = (iso) => format(day$6(iso), "MMM d");
|
|
3796
3796
|
/** The three numbers at the top: how late, how much work, how many risks came true. */
|
|
3797
3797
|
function voyageStats(log) {
|
|
@@ -3806,10 +3806,10 @@ function voyageStats(log) {
|
|
|
3806
3806
|
value: "On time",
|
|
3807
3807
|
label: "on the planned day"
|
|
3808
3808
|
} : days > 0 ? {
|
|
3809
|
-
value: `+${plural$
|
|
3809
|
+
value: `+${plural$5(days, "day")}`,
|
|
3810
3810
|
label: `past the ${plan}`
|
|
3811
3811
|
} : {
|
|
3812
|
-
value: plural$
|
|
3812
|
+
value: plural$5(-days, "day"),
|
|
3813
3813
|
label: `ahead of the ${plan}`
|
|
3814
3814
|
},
|
|
3815
3815
|
{
|
|
@@ -3835,7 +3835,7 @@ function voyageStory(log) {
|
|
|
3835
3835
|
out.push(`Barnacles: ${grew}${by}.${days ? ` ${days}` : ""}`);
|
|
3836
3836
|
}
|
|
3837
3837
|
for (const s of log.slips) {
|
|
3838
|
-
const late = `“${s.task.title}” finished ${plural$
|
|
3838
|
+
const late = `“${s.task.title}” finished ${plural$5(s.daysLate, "day")} late`;
|
|
3839
3839
|
out.push(s.waitedOn ? `${late}; it waited on “${s.waitedOn.title}”, done ${md$2(s.waitedOn.completedAt)}.` : `${late}.`);
|
|
3840
3840
|
}
|
|
3841
3841
|
for (const r of log.risks.filter((x) => x.happened)) out.push(`The risk “${r.risk.title}” came true${r.guessed ? " (an issue names it)" : ""}.`);
|
|
@@ -3843,11 +3843,11 @@ function voyageStory(log) {
|
|
|
3843
3843
|
if (against) out.push(against);
|
|
3844
3844
|
if (log.decisions.length) {
|
|
3845
3845
|
const changed = log.decisions.filter((d) => d.status === "superseded").length;
|
|
3846
|
-
out.push(`${plural$
|
|
3846
|
+
out.push(`${plural$5(log.decisions.length, "decision was", "decisions were")} written down${changed ? `; ${changed === 1 ? "one was" : `${changed} were`} changed later` : ""}.`);
|
|
3847
3847
|
}
|
|
3848
3848
|
const top = log.carriers[0];
|
|
3849
|
-
if (top && log.carriers.length > 1) out.push(`${top.person.name} closed ${plural$
|
|
3850
|
-
else if (top) out.push(`${top.person.name} closed ${plural$
|
|
3849
|
+
if (top && log.carriers.length > 1) out.push(`${top.person.name} closed ${plural$5(top.closed, "task")}, the most on the crew.`);
|
|
3850
|
+
else if (top) out.push(`${top.person.name} closed ${plural$5(top.closed, "task")}.`);
|
|
3851
3851
|
if (log.budget > 0) {
|
|
3852
3852
|
const pct = Math.round(log.spent / log.budget * 100);
|
|
3853
3853
|
out.push(`Spent ${pct}% of the budget.`);
|
|
@@ -5639,7 +5639,7 @@ function projectSummary(data, project) {
|
|
|
5639
5639
|
* A due date typed in the terminal: 2026-10-14, today, tomorrow, +3 (days
|
|
5640
5640
|
* from today), or none. Null when it can't be read; "" for none.
|
|
5641
5641
|
*/
|
|
5642
|
-
function readDue(text, today) {
|
|
5642
|
+
function readDue$1(text, today) {
|
|
5643
5643
|
const t = text.trim().toLowerCase();
|
|
5644
5644
|
if (t === "" || t === "none" || t === "-") return "";
|
|
5645
5645
|
if (t === "today") return today;
|
|
@@ -6443,7 +6443,7 @@ function Editor({ label, edit, done }) {
|
|
|
6443
6443
|
hint: error ?? "A date, today, tomorrow, +3 for three days on, or none",
|
|
6444
6444
|
onCancel: done,
|
|
6445
6445
|
onSubmit: (text) => {
|
|
6446
|
-
const due = readDue(text, today());
|
|
6446
|
+
const due = readDue$1(text, today());
|
|
6447
6447
|
if (due == null) return setError(`“${text}” isn't a date Harbor can read.`);
|
|
6448
6448
|
done();
|
|
6449
6449
|
edit.onSave(due || null);
|
|
@@ -6610,7 +6610,7 @@ function Stepper({ title, steps, width, onDone, onCancel }) {
|
|
|
6610
6610
|
const t = text.trim();
|
|
6611
6611
|
if (!t && !step.optional) return setError(`${step.label} is needed.`);
|
|
6612
6612
|
if (step.kind === "date") {
|
|
6613
|
-
const due = t ? readDue(t, today()) : "";
|
|
6613
|
+
const due = t ? readDue$1(t, today()) : "";
|
|
6614
6614
|
if (due == null) return setError(`“${text}” isn't a date Harbor can read.`);
|
|
6615
6615
|
return next(due || null);
|
|
6616
6616
|
}
|
|
@@ -8495,7 +8495,7 @@ function roomFor(store, projectId, status) {
|
|
|
8495
8495
|
//#endregion
|
|
8496
8496
|
//#region src/tui/views/common.tsx
|
|
8497
8497
|
/** One task on one line: key, title, due, priority, and where it lives. */
|
|
8498
|
-
function TaskLine({ task, projects, people, today, selected, width, showProject = true, showAssignee = false, blocked = false }) {
|
|
8498
|
+
function TaskLine({ task, projects, people, today, selected, picked = false, width, showProject = true, showAssignee = false, blocked = false }) {
|
|
8499
8499
|
const late = isLate(task, today);
|
|
8500
8500
|
const project = showProject ? projects.find((p) => p.id === task.projectId) : null;
|
|
8501
8501
|
const who = showAssignee ? people?.find((p) => p.id === task.assigneeId) : null;
|
|
@@ -8506,7 +8506,11 @@ function TaskLine({ task, projects, people, today, selected, width, showProject
|
|
|
8506
8506
|
bold: selected,
|
|
8507
8507
|
wrap: "truncate-end",
|
|
8508
8508
|
children: [
|
|
8509
|
-
selected ? "›
|
|
8509
|
+
selected ? "›" : " ",
|
|
8510
|
+
picked ? /* @__PURE__ */ jsx(Text, {
|
|
8511
|
+
color: theme.accent,
|
|
8512
|
+
children: "✓"
|
|
8513
|
+
}) : " ",
|
|
8510
8514
|
/* @__PURE__ */ jsx(Text, {
|
|
8511
8515
|
color: theme.muted,
|
|
8512
8516
|
children: keyOf$1({ projects }, task).padEnd(7)
|
|
@@ -10015,7 +10019,7 @@ function byTask(rows, who) {
|
|
|
10015
10019
|
}
|
|
10016
10020
|
return out;
|
|
10017
10021
|
}
|
|
10018
|
-
const plural$
|
|
10022
|
+
const plural$4 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
10019
10023
|
/** Work only one person has touched, soonest-needed first. `scope` narrows it to some programs. */
|
|
10020
10024
|
function lifeboats(data, now, opts = {}) {
|
|
10021
10025
|
const today = isoDay(now);
|
|
@@ -10053,7 +10057,7 @@ function lifeboats(data, now, opts = {}) {
|
|
|
10053
10057
|
kind: "project",
|
|
10054
10058
|
id: project.id,
|
|
10055
10059
|
label: project.name,
|
|
10056
|
-
detail: `${plural$
|
|
10060
|
+
detail: `${plural$4(open.length, "open task")}, nobody else on them`,
|
|
10057
10061
|
soloId: solo[0],
|
|
10058
10062
|
projectId: project.id,
|
|
10059
10063
|
programId: project.programId,
|
|
@@ -10075,7 +10079,7 @@ function lifeboats(data, now, opts = {}) {
|
|
|
10075
10079
|
out.push({
|
|
10076
10080
|
kind: "tasks",
|
|
10077
10081
|
id: `tasks:${project.id}:${soloId}`,
|
|
10078
|
-
label: `${plural$
|
|
10082
|
+
label: `${plural$4(list.length, "task")} on ${project.name}`,
|
|
10079
10083
|
detail: `only ${first} has touched them`,
|
|
10080
10084
|
soloId,
|
|
10081
10085
|
projectId: project.id,
|
|
@@ -10095,7 +10099,7 @@ function lifeboats(data, now, opts = {}) {
|
|
|
10095
10099
|
kind: "task",
|
|
10096
10100
|
id: t.id,
|
|
10097
10101
|
label: key ? `${key} ${t.title}` : t.title,
|
|
10098
|
-
detail: [d < today ? `late since ${md(d)}` : `due ${md(d)}`, holds ? `${plural$
|
|
10102
|
+
detail: [d < today ? `late since ${md(d)}` : `due ${md(d)}`, holds ? `${plural$4(holds, "task")} ${holds === 1 ? "waits" : "wait"} on it` : "urgent"].join(", "),
|
|
10099
10103
|
soloId,
|
|
10100
10104
|
projectId: project.id,
|
|
10101
10105
|
programId: project.programId,
|
|
@@ -10122,7 +10126,7 @@ function lifeboats(data, now, opts = {}) {
|
|
|
10122
10126
|
const programId = first.programId ?? project?.programId ?? null;
|
|
10123
10127
|
const where = project?.name ?? (programId ? programs.get(programId)?.name : null) ?? null;
|
|
10124
10128
|
const issues = items.filter((r) => r.kind === "issue").length;
|
|
10125
|
-
const noun = issues === 0 ? plural$
|
|
10129
|
+
const noun = issues === 0 ? plural$4(items.length, "risk") : issues === items.length ? plural$4(items.length, "issue") : `${plural$4(items.length - issues, "risk")} and ${plural$4(issues, "issue")}`;
|
|
10126
10130
|
out.push({
|
|
10127
10131
|
kind: "risks",
|
|
10128
10132
|
id: key,
|
|
@@ -10452,7 +10456,7 @@ function decisionFields(item, store, go, today) {
|
|
|
10452
10456
|
if (kind === null) return setRevisit(null);
|
|
10453
10457
|
if (kind === "date") return go.ask("Look again on (today, fri, +28 or 2026-11-02)", (text) => {
|
|
10454
10458
|
if (!text.trim()) return;
|
|
10455
|
-
const day = readDue(text, today);
|
|
10459
|
+
const day = readDue$1(text, today);
|
|
10456
10460
|
if (day === null) return go.notify("Can't read that date. Try +28 or 2026-11-02.");
|
|
10457
10461
|
setRevisit(day ? {
|
|
10458
10462
|
kind: "date",
|
|
@@ -18615,13 +18619,13 @@ function milestonePole(milestone, data, now) {
|
|
|
18615
18619
|
undated: first.pole.kind === "chain" ? first.pole.undated : 0
|
|
18616
18620
|
};
|
|
18617
18621
|
}
|
|
18618
|
-
const plural$
|
|
18622
|
+
const plural$3 = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
18619
18623
|
/** A link's slack in a few words: "4 days spare", "no slack", "3 days late for what's next". */
|
|
18620
18624
|
function slackText(slack, last = false) {
|
|
18621
18625
|
if (slack == null) return last ? "no date to hold" : "next has no due date";
|
|
18622
|
-
if (slack > 0) return `${plural$
|
|
18626
|
+
if (slack > 0) return `${plural$3(slack, "day")} spare`;
|
|
18623
18627
|
if (slack === 0) return "no slack";
|
|
18624
|
-
return last ? `runs ${plural$
|
|
18628
|
+
return last ? `runs ${plural$3(-slack, "day")} over` : `pushes the next ${plural$3(-slack, "day")}`;
|
|
18625
18629
|
}
|
|
18626
18630
|
/**
|
|
18627
18631
|
* The sentence above the line: what sets the date and how it compares with
|
|
@@ -18633,9 +18637,9 @@ function poleHeadline(pole, name, held) {
|
|
|
18633
18637
|
const lead = pole.via ? `${pole.via.project.name} finishes ${shortDate(pole.finish)}` : `${last ? name(last.task) : "It"} can finish ${shortDate(pole.finish)}`;
|
|
18634
18638
|
const tail = pole.via && last && last.finish < pole.finish ? ` Its tasks can all be done by ${shortDate(last.finish)}; its end date sets it.` : "";
|
|
18635
18639
|
if (pole.over == null || !pole.target) return `${lead}.${tail}`;
|
|
18636
|
-
if (pole.over > 0) return `${lead}, ${plural$
|
|
18640
|
+
if (pole.over > 0) return `${lead}, ${plural$3(pole.over, "day")} after ${held} (${shortDate(pole.target)}).${tail}`;
|
|
18637
18641
|
if (pole.over === 0) return `${lead}, right on ${held}.${tail}`;
|
|
18638
|
-
return `${lead}, ${plural$
|
|
18642
|
+
return `${lead}, ${plural$3(-pole.over, "day")} before ${held}.${tail}`;
|
|
18639
18643
|
}
|
|
18640
18644
|
/** "13 open tasks have no due date, so they can't hold the date."; null when none. */
|
|
18641
18645
|
function undatedText(pole) {
|
|
@@ -18958,14 +18962,17 @@ function weekPlan(data, personId, monday) {
|
|
|
18958
18962
|
};
|
|
18959
18963
|
});
|
|
18960
18964
|
const placed = new Set(inWeek.map((r) => r.taskId));
|
|
18961
|
-
const
|
|
18965
|
+
const open = data.tasks.filter((t) => t.assigneeId === personId && t.status !== "done" && !placed.has(t.id));
|
|
18966
|
+
const unplanned = open.filter((t) => t.status !== "review").sort(byDue);
|
|
18967
|
+
const inReview = open.filter((t) => t.status === "review").sort(byDue);
|
|
18962
18968
|
return {
|
|
18963
18969
|
monday,
|
|
18964
18970
|
days,
|
|
18965
18971
|
dayHours,
|
|
18966
18972
|
planned: days.reduce((sum, d) => sum + d.hours, 0),
|
|
18967
18973
|
capacity: dayHours * 5,
|
|
18968
|
-
unplanned
|
|
18974
|
+
unplanned,
|
|
18975
|
+
inReview
|
|
18969
18976
|
};
|
|
18970
18977
|
}
|
|
18971
18978
|
/** Planned against done for a week: what was on the plan, what got finished, and what came in besides. */
|
|
@@ -19304,6 +19311,8 @@ function taskLine(t, people, projects, all) {
|
|
|
19304
19311
|
};
|
|
19305
19312
|
}
|
|
19306
19313
|
/** One person's week plan as the agent tools give it (get_week, plan_week). */
|
|
19314
|
+
/** Most of the tasks not on a day that get_week lists; the count says how many there are. */
|
|
19315
|
+
const UNPLANNED_MAX = 200;
|
|
19307
19316
|
function weekOut(data, personId, monday) {
|
|
19308
19317
|
const people = byId(data.people);
|
|
19309
19318
|
const projects = byId(data.projects);
|
|
@@ -19328,8 +19337,9 @@ function weekOut(data, personId, monday) {
|
|
|
19328
19337
|
...taskLine(item.task, people, projects, data.tasks)
|
|
19329
19338
|
}))
|
|
19330
19339
|
})),
|
|
19331
|
-
unplanned: plan.unplanned.slice(0,
|
|
19340
|
+
unplanned: plan.unplanned.slice(0, 200).map((t) => taskLine(t, people, projects, data.tasks)),
|
|
19332
19341
|
unplannedCount: plan.unplanned.length,
|
|
19342
|
+
inReviewCount: plan.inReview.length,
|
|
19333
19343
|
review: {
|
|
19334
19344
|
summary: reviewText(review),
|
|
19335
19345
|
planned: review.planned.length,
|
|
@@ -20982,6 +20992,7 @@ function reversalOf(before, after) {
|
|
|
20982
20992
|
}
|
|
20983
20993
|
function labelOf(before, after, forward) {
|
|
20984
20994
|
const lead = LEAD.map((key) => forward.filter((f) => f.key === key)).find((l) => l.length > 0);
|
|
20995
|
+
if (lead && lead.length > 1 && lead.every((f) => f.key === "tasks" && f.change.kind === "set")) return `your change to ${lead.length} tasks`;
|
|
20985
20996
|
if (!lead || lead.length !== 1) return "your last change";
|
|
20986
20997
|
const { key, change } = lead[0];
|
|
20987
20998
|
const find = (doc, id) => (doc[key] ?? []).find((r) => r.id === id);
|
|
@@ -22005,6 +22016,61 @@ function stampAssignment(before, next, by, at) {
|
|
|
22005
22016
|
}
|
|
22006
22017
|
var init_assignment = __esmMin((() => {}));
|
|
22007
22018
|
|
|
22019
|
+
//#endregion
|
|
22020
|
+
//#region src/lib/bulk.ts
|
|
22021
|
+
/** A task's due date moved by `days`, or null when it has none. */
|
|
22022
|
+
function shiftedDue(due, days) {
|
|
22023
|
+
if (!due) return null;
|
|
22024
|
+
return todayISO(addDays(parseIso(`${due.slice(0, 10)}T12:00:00`), days));
|
|
22025
|
+
}
|
|
22026
|
+
/** The task as the change leaves it, before automations and stamps; the same task when nothing moves. */
|
|
22027
|
+
function applyBulk(task, change) {
|
|
22028
|
+
if ("status" in change) return task.status === change.status ? task : {
|
|
22029
|
+
...task,
|
|
22030
|
+
status: change.status
|
|
22031
|
+
};
|
|
22032
|
+
if ("assigneeId" in change) return task.assigneeId === change.assigneeId ? task : {
|
|
22033
|
+
...task,
|
|
22034
|
+
assigneeId: change.assigneeId
|
|
22035
|
+
};
|
|
22036
|
+
if ("dueDate" in change) return task.dueDate === change.dueDate ? task : {
|
|
22037
|
+
...task,
|
|
22038
|
+
dueDate: change.dueDate
|
|
22039
|
+
};
|
|
22040
|
+
if ("shiftDays" in change) {
|
|
22041
|
+
const due = shiftedDue(task.dueDate, change.shiftDays);
|
|
22042
|
+
return due === null || change.shiftDays === 0 ? task : {
|
|
22043
|
+
...task,
|
|
22044
|
+
dueDate: due
|
|
22045
|
+
};
|
|
22046
|
+
}
|
|
22047
|
+
return task.priority === change.priority ? task : {
|
|
22048
|
+
...task,
|
|
22049
|
+
priority: change.priority
|
|
22050
|
+
};
|
|
22051
|
+
}
|
|
22052
|
+
/** What the change did, for the Activity line and the tray's notice: "moved 4 tasks to In play". */
|
|
22053
|
+
function bulkText(change, n, names = {}) {
|
|
22054
|
+
const tasks = plural$2(n);
|
|
22055
|
+
if ("status" in change) return change.status === "done" ? `finished ${tasks}` : `moved ${tasks} to ${TASK_STATUS_LABEL[change.status]}`;
|
|
22056
|
+
if ("assigneeId" in change) return change.assigneeId ? `gave ${tasks} to ${names.person ?? "someone"}` : `unassigned ${tasks}`;
|
|
22057
|
+
if ("dueDate" in change) return change.dueDate ? `set ${tasks} due ${change.dueDate}` : `cleared the due date of ${tasks}`;
|
|
22058
|
+
if ("shiftDays" in change) {
|
|
22059
|
+
const weeks = Math.abs(change.shiftDays) % 7 === 0 ? Math.abs(change.shiftDays) / 7 : null;
|
|
22060
|
+
const by = weeks ? `${weeks} ${weeks === 1 ? "week" : "weeks"}` : `${Math.abs(change.shiftDays)} days`;
|
|
22061
|
+
return `${change.shiftDays > 0 ? "pushed" : "pulled"} ${tasks} ${by} ${change.shiftDays > 0 ? "later" : "earlier"}`;
|
|
22062
|
+
}
|
|
22063
|
+
return `set ${tasks} to ${PRIORITY_LABEL[change.priority].toLowerCase()} priority`;
|
|
22064
|
+
}
|
|
22065
|
+
var plural$2;
|
|
22066
|
+
var init_bulk = __esmMin((() => {
|
|
22067
|
+
init_date_fns();
|
|
22068
|
+
init_date_text();
|
|
22069
|
+
init_format();
|
|
22070
|
+
init_swatches();
|
|
22071
|
+
plural$2 = (n) => `${n} ${n === 1 ? "task" : "tasks"}`;
|
|
22072
|
+
}));
|
|
22073
|
+
|
|
22008
22074
|
//#endregion
|
|
22009
22075
|
//#region src/lib/slices/tasks.ts
|
|
22010
22076
|
function tasksSlice({ set, get, write }) {
|
|
@@ -22326,6 +22392,70 @@ function tasksSlice({ set, get, write }) {
|
|
|
22326
22392
|
} : t) });
|
|
22327
22393
|
get().log(kept.length === 1 ? `kept “${kept[0].title}” out of the fog` : `kept ${kept.length} tasks out of the fog`, kept.length === 1 ? `/projects/${kept[0].projectId}` : void 0);
|
|
22328
22394
|
},
|
|
22395
|
+
editTasks: (ids, change, opts) => {
|
|
22396
|
+
const s = get();
|
|
22397
|
+
const pick = new Set(ids.slice(0, 500));
|
|
22398
|
+
const moving = s.tasks.filter((t) => pick.has(t.id)).filter((t) => applyBulk(t, change) !== t);
|
|
22399
|
+
if (moving.length === 0) return { changed: 0 };
|
|
22400
|
+
if ("status" in change) {
|
|
22401
|
+
if (change.status === "done" && !opts?.allowBlocked) {
|
|
22402
|
+
const blocked = moving.filter((t) => isBlocked$1(t, s.tasks)).length;
|
|
22403
|
+
if (blocked) return {
|
|
22404
|
+
changed: 0,
|
|
22405
|
+
blocked
|
|
22406
|
+
};
|
|
22407
|
+
}
|
|
22408
|
+
if (!opts?.allowOverCap) {
|
|
22409
|
+
const into = /* @__PURE__ */ new Map();
|
|
22410
|
+
for (const t of moving) into.set(t.projectId, (into.get(t.projectId) ?? 0) + 1);
|
|
22411
|
+
for (const [projectId, n] of into) {
|
|
22412
|
+
const overCap = columnWip(s, projectId, change.status, n);
|
|
22413
|
+
if (overCap) return {
|
|
22414
|
+
changed: 0,
|
|
22415
|
+
overCap
|
|
22416
|
+
};
|
|
22417
|
+
}
|
|
22418
|
+
}
|
|
22419
|
+
}
|
|
22420
|
+
const me = s.currentUserId;
|
|
22421
|
+
const at = stamp();
|
|
22422
|
+
const projects = new Map(s.projects.map((p) => [p.id, p]));
|
|
22423
|
+
const newComments = [];
|
|
22424
|
+
const follows = [];
|
|
22425
|
+
const moved = new Set(moving.map((t) => t.id));
|
|
22426
|
+
const tasks = s.tasks.map((row) => {
|
|
22427
|
+
if (!moved.has(row.id)) return row;
|
|
22428
|
+
let next = applyBulk(row, change);
|
|
22429
|
+
if ("status" in change) {
|
|
22430
|
+
next = withStatus(row, change.status);
|
|
22431
|
+
if (change.status === "done" && isBlocked$1(row, s.tasks)) Object.assign(next, blockersPatch([]));
|
|
22432
|
+
}
|
|
22433
|
+
const { task: automated, comments } = applyAutomations(projects.get(row.projectId), row, next, {
|
|
22434
|
+
created: false,
|
|
22435
|
+
currentUserId: me
|
|
22436
|
+
});
|
|
22437
|
+
newComments.push(...comments.map((c) => ({
|
|
22438
|
+
...c,
|
|
22439
|
+
id: uid("c")
|
|
22440
|
+
})));
|
|
22441
|
+
const final = stampAssignment(row, automated, me, at);
|
|
22442
|
+
if (final.status === "done" && row.status !== "done") {
|
|
22443
|
+
const follow = nextOccurrence(final, uid("t"), todayISO(), at);
|
|
22444
|
+
if (follow) follows.push(follow);
|
|
22445
|
+
}
|
|
22446
|
+
return final;
|
|
22447
|
+
});
|
|
22448
|
+
write({
|
|
22449
|
+
tasks: follows.length ? [...tasks, ...follows] : tasks,
|
|
22450
|
+
comments: newComments.length ? [...s.comments, ...newComments] : s.comments,
|
|
22451
|
+
wipPending: null,
|
|
22452
|
+
blockPending: null
|
|
22453
|
+
});
|
|
22454
|
+
const person = "assigneeId" in change ? s.people.find((p) => p.id === change.assigneeId)?.name : null;
|
|
22455
|
+
const where = new Set(moving.map((t) => t.projectId));
|
|
22456
|
+
get().log(bulkText(change, moving.length, { person }), where.size === 1 ? `/projects/${[...where][0]}` : void 0);
|
|
22457
|
+
return { changed: moving.length };
|
|
22458
|
+
},
|
|
22329
22459
|
toggleFocus: (taskId) => {
|
|
22330
22460
|
const personId = get().currentUserId;
|
|
22331
22461
|
const date = todayISO();
|
|
@@ -22429,6 +22559,7 @@ var init_tasks = __esmMin((() => {
|
|
|
22429
22559
|
init_shared();
|
|
22430
22560
|
init_recurrence();
|
|
22431
22561
|
init_assignment();
|
|
22562
|
+
init_bulk();
|
|
22432
22563
|
}));
|
|
22433
22564
|
|
|
22434
22565
|
//#endregion
|
|
@@ -26811,6 +26942,117 @@ function PortfolioView({ portfolioId, active, width, height, go, onBack }) {
|
|
|
26811
26942
|
});
|
|
26812
26943
|
}
|
|
26813
26944
|
|
|
26945
|
+
//#endregion
|
|
26946
|
+
//#region src/tui/bulk.ts
|
|
26947
|
+
init_bulk();
|
|
26948
|
+
init_board();
|
|
26949
|
+
init_swatches();
|
|
26950
|
+
/** "2026-11-04", "today", "tomorrow", "+3", or "none"; undefined when it's none of those. */
|
|
26951
|
+
function readDue(text, today) {
|
|
26952
|
+
const t = text.trim().toLowerCase();
|
|
26953
|
+
if (t === "none" || t === "no date") return null;
|
|
26954
|
+
if (t === "today") return today;
|
|
26955
|
+
if (t === "tomorrow") return shiftedDue(today, 1);
|
|
26956
|
+
if (/^\+\d{1,3}$/.test(t)) return shiftedDue(today, Number(t.slice(1)));
|
|
26957
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(t)) return t;
|
|
26958
|
+
}
|
|
26959
|
+
function applyPicked(store, go, ids, change, opts) {
|
|
26960
|
+
const s = store.getState();
|
|
26961
|
+
const r = s.editTasks(ids, change, opts);
|
|
26962
|
+
if (r.blocked) return go.confirm(`${r.blocked === 1 ? "One of them still waits" : `${r.blocked} of them still wait`} on other tasks. Mark them all done anyway?`, () => applyPicked(store, go, ids, change, {
|
|
26963
|
+
...opts,
|
|
26964
|
+
allowBlocked: true
|
|
26965
|
+
}), "Mark done");
|
|
26966
|
+
if (r.overCap) {
|
|
26967
|
+
if (s.myRole !== "owner") return go.notify(wipFullMessage(r.overCap));
|
|
26968
|
+
return go.confirm(`${wipFullMessage(r.overCap)} Move them past the limit?`, () => applyPicked(store, go, ids, change, {
|
|
26969
|
+
...opts,
|
|
26970
|
+
allowOverCap: true
|
|
26971
|
+
}), "Move anyway");
|
|
26972
|
+
}
|
|
26973
|
+
if (r.changed === 0) return go.notify("Nothing to change: they're all like that already.");
|
|
26974
|
+
const person = "assigneeId" in change ? s.people.find((p) => p.id === change.assigneeId)?.name : null;
|
|
26975
|
+
const text = bulkText(change, r.changed, { person });
|
|
26976
|
+
go.notify(`${text[0].toUpperCase()}${text.slice(1)}. u undoes it.`);
|
|
26977
|
+
}
|
|
26978
|
+
/** What m offers for the picked tasks; `done` runs after a change so the view can let them go. */
|
|
26979
|
+
function bulkMenu(store, go, ids, today, clear) {
|
|
26980
|
+
const run = (change) => applyPicked(store, go, ids, change);
|
|
26981
|
+
const n = ids.length;
|
|
26982
|
+
go.pick(`${n} picked`, [
|
|
26983
|
+
{
|
|
26984
|
+
label: "Move to…",
|
|
26985
|
+
value: "move"
|
|
26986
|
+
},
|
|
26987
|
+
{
|
|
26988
|
+
label: "Assign to…",
|
|
26989
|
+
value: "assign"
|
|
26990
|
+
},
|
|
26991
|
+
{
|
|
26992
|
+
label: "Due date…",
|
|
26993
|
+
value: "due"
|
|
26994
|
+
},
|
|
26995
|
+
{
|
|
26996
|
+
label: "Push a week later",
|
|
26997
|
+
value: "push",
|
|
26998
|
+
hint: "undated ones stay"
|
|
26999
|
+
},
|
|
27000
|
+
{
|
|
27001
|
+
label: "Priority…",
|
|
27002
|
+
value: "priority"
|
|
27003
|
+
},
|
|
27004
|
+
{
|
|
27005
|
+
label: "Mark done",
|
|
27006
|
+
value: "done"
|
|
27007
|
+
},
|
|
27008
|
+
{
|
|
27009
|
+
label: "Let go of them",
|
|
27010
|
+
value: "clear",
|
|
27011
|
+
hint: "esc"
|
|
27012
|
+
}
|
|
27013
|
+
], (what) => {
|
|
27014
|
+
if (what === "clear") return clear();
|
|
27015
|
+
if (what === "push") return run({ shiftDays: 7 });
|
|
27016
|
+
if (what === "done") return run({ status: "done" });
|
|
27017
|
+
if (what === "move") return go.pick(`Move ${n} to`, TASK_COLUMNS.map((st) => ({
|
|
27018
|
+
label: TASK_STATUS_LABEL[st],
|
|
27019
|
+
value: st
|
|
27020
|
+
})), (status) => run({ status }));
|
|
27021
|
+
if (what === "priority") return go.pick(`Priority for ${n}`, [
|
|
27022
|
+
"urgent",
|
|
27023
|
+
"high",
|
|
27024
|
+
"medium",
|
|
27025
|
+
"low"
|
|
27026
|
+
].map((p) => ({
|
|
27027
|
+
label: PRIORITY_LABEL[p],
|
|
27028
|
+
value: p
|
|
27029
|
+
})), (priority) => run({ priority }));
|
|
27030
|
+
if (what === "assign") {
|
|
27031
|
+
const s = store.getState();
|
|
27032
|
+
const crew = [...s.people].filter((p) => p.id !== s.currentUserId).sort((a, b) => a.name.localeCompare(b.name));
|
|
27033
|
+
return go.pick(`Give ${n} to`, [
|
|
27034
|
+
{
|
|
27035
|
+
label: "Me",
|
|
27036
|
+
value: s.currentUserId
|
|
27037
|
+
},
|
|
27038
|
+
{
|
|
27039
|
+
label: "Nobody",
|
|
27040
|
+
value: null
|
|
27041
|
+
},
|
|
27042
|
+
...crew.map((p) => ({
|
|
27043
|
+
label: p.name,
|
|
27044
|
+
value: p.id
|
|
27045
|
+
}))
|
|
27046
|
+
], (who) => run({ assigneeId: who }));
|
|
27047
|
+
}
|
|
27048
|
+
if (what === "due") return go.ask("Due (YYYY-MM-DD, today, tomorrow, +7 or none)", (text) => {
|
|
27049
|
+
const due = readDue(text, today);
|
|
27050
|
+
if (due === void 0) return go.notify(`"${text}" isn't a date Harbor reads.`);
|
|
27051
|
+
run({ dueDate: due });
|
|
27052
|
+
});
|
|
27053
|
+
});
|
|
27054
|
+
}
|
|
27055
|
+
|
|
26814
27056
|
//#endregion
|
|
26815
27057
|
//#region src/tui/views/project.tsx
|
|
26816
27058
|
init_swatches();
|
|
@@ -26864,6 +27106,9 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
|
|
|
26864
27106
|
const [col, setCol] = useState(() => Math.max(0, columns.findIndex((c) => c.tasks.length > 0)));
|
|
26865
27107
|
const [rowByCol, setRowByCol] = useState({});
|
|
26866
27108
|
const [listAt, setListAt] = useState(0);
|
|
27109
|
+
const [pickedIds, setPicked] = useState([]);
|
|
27110
|
+
const live = useMemo(() => new Set(tasks.map((t) => t.id)), [tasks]);
|
|
27111
|
+
const picked = pickedIds.filter((id) => live.has(id));
|
|
26867
27112
|
const day = today();
|
|
26868
27113
|
const fogWeeks = (t) => {
|
|
26869
27114
|
if (t.status === "done") return null;
|
|
@@ -26893,7 +27138,10 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
|
|
|
26893
27138
|
}
|
|
26894
27139
|
};
|
|
26895
27140
|
useInput((input, key) => {
|
|
27141
|
+
if (key.escape && picked.length) return setPicked([]);
|
|
26896
27142
|
if (key.escape || key.backspace) return onBack();
|
|
27143
|
+
if (input === " " && selected && mode !== "budget") return setPicked((ids) => ids.includes(selected.id) ? ids.filter((x) => x !== selected.id) : [...ids, selected.id]);
|
|
27144
|
+
if (input === "m" && picked.length && mode !== "budget") return bulkMenu(store, go, picked, day, () => setPicked([]));
|
|
26897
27145
|
if (input === "v") return setMode((m) => m === "board" ? "list" : m === "list" ? "budget" : "board");
|
|
26898
27146
|
if (input === "e") return go.openPanel("project", projectId);
|
|
26899
27147
|
if (input === "b") return go.openPanel("board", projectId);
|
|
@@ -27048,6 +27296,16 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
|
|
|
27048
27296
|
]
|
|
27049
27297
|
}) : null,
|
|
27050
27298
|
" ",
|
|
27299
|
+
picked.length && mode !== "budget" ? /* @__PURE__ */ jsxs(Text, {
|
|
27300
|
+
color: theme.accent,
|
|
27301
|
+
children: [
|
|
27302
|
+
" ",
|
|
27303
|
+
"· ",
|
|
27304
|
+
picked.length,
|
|
27305
|
+
" picked: m changes them, esc lets go"
|
|
27306
|
+
]
|
|
27307
|
+
}) : " · space picks",
|
|
27308
|
+
" ",
|
|
27051
27309
|
"· v next · f p filter · e details · b setup"
|
|
27052
27310
|
]
|
|
27053
27311
|
})
|
|
@@ -27182,7 +27440,8 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
|
|
|
27182
27440
|
waits: blocked(data, t),
|
|
27183
27441
|
priority: t.priority,
|
|
27184
27442
|
selected: on && view.start + i === r,
|
|
27185
|
-
fog: fogWeeks(t)
|
|
27443
|
+
fog: fogWeeks(t),
|
|
27444
|
+
picked: picked.includes(t.id)
|
|
27186
27445
|
}, t.id)),
|
|
27187
27446
|
view.start + view.items.length < c.tasks.length ? /* @__PURE__ */ jsxs(Text, {
|
|
27188
27447
|
color: theme.muted,
|
|
@@ -27212,6 +27471,7 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
|
|
|
27212
27471
|
people,
|
|
27213
27472
|
today: day,
|
|
27214
27473
|
selected: active && start + i === listAt,
|
|
27474
|
+
picked: picked.includes(t.id),
|
|
27215
27475
|
width: width - 12,
|
|
27216
27476
|
showProject: false,
|
|
27217
27477
|
showAssignee: true,
|
|
@@ -27223,7 +27483,7 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
|
|
|
27223
27483
|
});
|
|
27224
27484
|
}
|
|
27225
27485
|
/** One card on the board: its key, due date and owner, then the title on up to two lines. */
|
|
27226
|
-
function Card({ taskKey, title, owner, due, late, waits, priority, selected, fog = null }) {
|
|
27486
|
+
function Card({ taskKey, title, owner, due, late, waits, priority, selected, fog = null, picked = false }) {
|
|
27227
27487
|
return /* @__PURE__ */ jsxs(Box, {
|
|
27228
27488
|
flexDirection: "column",
|
|
27229
27489
|
height: 3,
|
|
@@ -27232,6 +27492,10 @@ function Card({ taskKey, title, owner, due, late, waits, priority, selected, fog
|
|
|
27232
27492
|
inverse: selected,
|
|
27233
27493
|
bold: selected,
|
|
27234
27494
|
children: [
|
|
27495
|
+
picked ? /* @__PURE__ */ jsx(Text, {
|
|
27496
|
+
color: selected ? void 0 : theme.accent,
|
|
27497
|
+
children: "✓ "
|
|
27498
|
+
}) : null,
|
|
27235
27499
|
/* @__PURE__ */ jsx(Text, {
|
|
27236
27500
|
color: selected ? void 0 : theme.muted,
|
|
27237
27501
|
children: taskKey
|
|
@@ -28487,7 +28751,7 @@ function DueField({ initial, today, onSubmit, onCancel }) {
|
|
|
28487
28751
|
hint: error ?? "A date, today, tomorrow, +3 for three days on, or none",
|
|
28488
28752
|
onCancel,
|
|
28489
28753
|
onSubmit: (text) => {
|
|
28490
|
-
const due = readDue(text, today);
|
|
28754
|
+
const due = readDue$1(text, today);
|
|
28491
28755
|
if (due == null) setError(`“${text}” isn't a date Harbor can read.`);
|
|
28492
28756
|
else onSubmit(due);
|
|
28493
28757
|
}
|
|
@@ -29228,6 +29492,8 @@ function App({ ui, startIn }) {
|
|
|
29228
29492
|
["n", "new"],
|
|
29229
29493
|
["[ ]", "move card"],
|
|
29230
29494
|
["x", "done"],
|
|
29495
|
+
["space", "pick"],
|
|
29496
|
+
["m", "change picked"],
|
|
29231
29497
|
["d", "delete"],
|
|
29232
29498
|
["v", "board/list/budget"],
|
|
29233
29499
|
["f p", "filter"],
|
package/lib/cli.mjs
CHANGED
|
@@ -145,10 +145,11 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
145
145
|
project as that one really ran
|
|
146
146
|
harbor add "TITLE" --project P [--assignee me|none|NAME]
|
|
147
147
|
[--due DATE|pilot] [--priority low|medium|high|urgent]
|
|
148
|
-
[--status S] [--comment TEXT] [--over-limit]
|
|
148
|
+
[--status S] [--comment TEXT] [--over-limit] [--dry-run]
|
|
149
149
|
with no TITLE: one title a line
|
|
150
150
|
from stdin; --due pilot takes the
|
|
151
151
|
date the owner's queue suggests
|
|
152
|
+
(harbor help add)
|
|
152
153
|
harbor start TASK | submit TASK | done TASK [--confirm] [--over-limit]
|
|
153
154
|
move a task to In play, In review
|
|
154
155
|
or Done
|
|
@@ -163,9 +164,10 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
163
164
|
your own week: tasks on days
|
|
164
165
|
against your hours, only for
|
|
165
166
|
you; --review: planned vs done
|
|
166
|
-
harbor week TASK DAY [--hours N]
|
|
167
|
-
harbor week TASK --off [DAY]
|
|
167
|
+
harbor week TASK --day DAY [--hours N] put a task on a day: mon to fri
|
|
168
|
+
harbor week TASK --off [--day DAY] take it off that day, or the week
|
|
168
169
|
harbor week --day-hours N the hours you work a day
|
|
170
|
+
(harbor help week)
|
|
169
171
|
harbor comment TASK "TEXT"
|
|
170
172
|
harbor checkin "WHAT YOU'RE DOING" [--on-blocker]
|
|
171
173
|
harbor risk "TITLE" --project P|--program P [--impact 1-5] [--probability 1-5]
|
|
@@ -555,6 +557,34 @@ export function voyageForTerminal(r, project = r.project) {
|
|
|
555
557
|
|
|
556
558
|
/** Commands whose forms do different things: one line each, saying which read and which write. */
|
|
557
559
|
const DETAIL = {
|
|
560
|
+
add: [
|
|
561
|
+
' harbor add "TITLE" --project P writes: one task, yours unless',
|
|
562
|
+
" --assignee says otherwise",
|
|
563
|
+
" harbor add --project P < titles.txt writes: one task a line of stdin",
|
|
564
|
+
" harbor add --project P --dry-run reads: says what it would add, and",
|
|
565
|
+
" adds nothing",
|
|
566
|
+
"",
|
|
567
|
+
" --assignee me|none|NAME, --due DATE|pilot, --priority low|medium|high|urgent,",
|
|
568
|
+
" --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. --due pilot",
|
|
570
|
+
" takes the date the owner's queue suggests. More than 100 lines is harbor",
|
|
571
|
+
" import's job.",
|
|
572
|
+
],
|
|
573
|
+
week: [
|
|
574
|
+
" harbor week [--next|--last|--week DATE] reads: your week, the days and",
|
|
575
|
+
" their hours, and what's not on a",
|
|
576
|
+
" day yet",
|
|
577
|
+
" harbor week --review reads: what you planned against",
|
|
578
|
+
" what you finished",
|
|
579
|
+
" harbor week TASK --day DAY [--hours N] writes: puts the task on that day",
|
|
580
|
+
" (mon to fri), 2 hours unless --hours",
|
|
581
|
+
" harbor week TASK --off [--day DAY] writes: takes it off that day, or",
|
|
582
|
+
" off the week",
|
|
583
|
+
" harbor week --day-hours N writes: the hours you work a day",
|
|
584
|
+
"",
|
|
585
|
+
" Only you see your week. Tasks in review are left out of what's not on a",
|
|
586
|
+
" day yet, since they wait on someone else.",
|
|
587
|
+
],
|
|
558
588
|
init: [
|
|
559
589
|
" harbor init [PROJECT] [--hook] writes: .harbor/ at the top of this",
|
|
560
590
|
" repository, one file per task in",
|
|
@@ -2105,7 +2135,10 @@ export async function run(argv, io = {}) {
|
|
|
2105
2135
|
}
|
|
2106
2136
|
case "add": {
|
|
2107
2137
|
const { positionals, values, harbor } = parse(
|
|
2108
|
-
spec(
|
|
2138
|
+
spec(
|
|
2139
|
+
["over-limit", "dry-run"],
|
|
2140
|
+
["project", "assignee", "due", "priority", "status", "comment"],
|
|
2141
|
+
),
|
|
2109
2142
|
);
|
|
2110
2143
|
if (!values.project) throw new HarborError("Say which project: --project NAME.");
|
|
2111
2144
|
// Several at once: one title a line on stdin, with no title (or -) after add.
|
|
@@ -2125,6 +2158,17 @@ export async function run(argv, io = {}) {
|
|
|
2125
2158
|
throw new HarborError(
|
|
2126
2159
|
"That's more than 100 tasks; harbor import FILE takes a spreadsheet.",
|
|
2127
2160
|
);
|
|
2161
|
+
if (values["dry-run"]) {
|
|
2162
|
+
const n = titles.length === 1 ? "1 task" : `${titles.length} tasks`;
|
|
2163
|
+
if (asJson) out(jsonText({ dryRun: true, project: values.project, titles }));
|
|
2164
|
+
else
|
|
2165
|
+
out(
|
|
2166
|
+
plain(
|
|
2167
|
+
[`Would add ${n} to ${values.project}:`, ...titles.map((t) => ` ${t}`)].join("\n"),
|
|
2168
|
+
),
|
|
2169
|
+
);
|
|
2170
|
+
return 0;
|
|
2171
|
+
}
|
|
2128
2172
|
const added = [];
|
|
2129
2173
|
for (const title of titles) {
|
|
2130
2174
|
const r = await client
|
|
@@ -2290,7 +2334,7 @@ export async function run(argv, io = {}) {
|
|
|
2290
2334
|
}
|
|
2291
2335
|
case "week": {
|
|
2292
2336
|
const { positionals, values, harbor } = parse(
|
|
2293
|
-
spec(["next", "last", "review", "off"], ["week", "hours", "day-hours"]),
|
|
2337
|
+
spec(["next", "last", "review", "off"], ["week", "hours", "day-hours", "day"]),
|
|
2294
2338
|
);
|
|
2295
2339
|
if ([values.next, values.last, values.week].filter(Boolean).length > 1)
|
|
2296
2340
|
throw new HarborError("Say one of --next, --last or --week DATE.");
|
|
@@ -2305,21 +2349,24 @@ export async function run(argv, io = {}) {
|
|
|
2305
2349
|
throw new HarborError(`--${name} is a number of hours, like 2 or 1.5.`);
|
|
2306
2350
|
return n;
|
|
2307
2351
|
};
|
|
2308
|
-
|
|
2309
|
-
|
|
2352
|
+
// The day comes as --day; a bare second word still works, as it did first.
|
|
2353
|
+
const [task, bareDay, ...extra] = positionals;
|
|
2354
|
+
if (extra.length || (bareDay && values.day))
|
|
2355
|
+
throw new HarborError("Give a task key and a day: harbor week RR-42 --day wed.");
|
|
2356
|
+
const day = values.day ?? bareDay;
|
|
2310
2357
|
const dayHours = number(values["day-hours"], "day-hours");
|
|
2311
2358
|
const show = (r) =>
|
|
2312
2359
|
[r.done, weekText(r, { ...opts, review: Boolean(values.review) })]
|
|
2313
2360
|
.filter(Boolean)
|
|
2314
2361
|
.join("\n\n");
|
|
2315
2362
|
if (!task && dayHours === undefined) {
|
|
2316
|
-
if (values.off || values.hours)
|
|
2317
|
-
throw new HarborError("Say which task: harbor week RR-42 wed --hours 3.");
|
|
2363
|
+
if (values.off || values.hours || values.day)
|
|
2364
|
+
throw new HarborError("Say which task: harbor week RR-42 --day wed --hours 3.");
|
|
2318
2365
|
return call("get_week", { harbor, week }, show);
|
|
2319
2366
|
}
|
|
2320
2367
|
if (task && !day && !values.off)
|
|
2321
2368
|
throw new HarborError(
|
|
2322
|
-
`Say which day for ${task}: harbor week ${task} wed, or --off to take it off the week.`,
|
|
2369
|
+
`Say which day for ${task}: harbor week ${task} --day wed, or --off to take it off the week.`,
|
|
2323
2370
|
);
|
|
2324
2371
|
return call(
|
|
2325
2372
|
"plan_week",
|
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;
|