@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/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] put a task on a day: mon to fri
167
- harbor week TASK --off [DAY] take it off that day, or the week
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(["over-limit"], ["project", "assignee", "due", "priority", "status", "comment"]),
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
- const [task, day, ...extra] = positionals;
2309
- if (extra.length) throw new HarborError("Give a task key and a day: harbor week RR-42 wed.");
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) out.push(` … and ${w.unplannedCount - 5} more (--json lists 25)`);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.61.0",
3
+ "version": "0.63.0",
4
4
  "description": "Harbor from the terminal: a full-screen app with everything, plus commands for scripts and agents.",
5
5
  "keywords": [
6
6
  "harbor",