@harborppm/cli 0.72.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/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
- with the date its owner's queue
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
- [--due DATE|pilot] [--priority low|medium|high|urgent]
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]
@@ -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, --due DATE|pilot, --priority low|medium|high|urgent,",
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.",
@@ -2088,6 +2112,25 @@ export async function run(argv, io = {}) {
2088
2112
  out(asJson ? jsonText(result) : plain(taskList(result, opts)));
2089
2113
  return 0;
2090
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
+ }
2091
2134
  case "show": {
2092
2135
  const { positionals, harbor } = parse(spec());
2093
2136
  return call("get_task", { harbor, task: one(positionals, "task key, like RR-42") }, (t) => {
@@ -2097,6 +2140,7 @@ export async function run(argv, io = {}) {
2097
2140
  { ...opts, width: undefined },
2098
2141
  ),
2099
2142
  ];
2143
+ if (t.alsoOn?.length) lines.push(`Also on it: ${t.alsoOn.join(", ")}.`);
2100
2144
  if (t.repeats) lines.push(`Repeats ${REPEAT_WORD[t.repeats] ?? t.repeats}.`);
2101
2145
  if (t.pilotDue?.fits) lines.push(`${t.pilotDue.says}, so the due date holds.`);
2102
2146
  else if (t.pilotDue) lines.push(...pilotLines({ pilot: t.pilotDue }, t));
@@ -2119,6 +2163,7 @@ export async function run(argv, io = {}) {
2119
2163
  (w) => ` ${w.key ?? w.id} ${w.title} (${STATUS[w.status] ?? w.status})`,
2120
2164
  ),
2121
2165
  );
2166
+ if (t.files?.length) lines.push("", "Files:", ...t.files.map((f) => ` ${fileText(f)}`));
2122
2167
  if (t.comments.length)
2123
2168
  lines.push(
2124
2169
  "",
@@ -2337,7 +2382,7 @@ export async function run(argv, io = {}) {
2337
2382
  const { positionals, values, harbor } = parse(
2338
2383
  spec(
2339
2384
  ["over-limit", "dry-run"],
2340
- ["project", "assignee", "due", "priority", "status", "comment"],
2385
+ ["project", "assignee", "also", "due", "priority", "status", "comment"],
2341
2386
  ),
2342
2387
  );
2343
2388
  // No --project: the harbor's one open project, if that's all there is.
@@ -2413,6 +2458,7 @@ export async function run(argv, io = {}) {
2413
2458
  title,
2414
2459
  // Yours unless you say otherwise, as with n in the app.
2415
2460
  assignee: values.assignee ?? "me",
2461
+ also: values.also === undefined ? undefined : alsoList(values.also),
2416
2462
  due: duePilot(values.due) ? undefined : dueDate(values.due),
2417
2463
  suggestedDue: duePilot(values.due),
2418
2464
  priority: values.priority,
@@ -2526,7 +2572,17 @@ export async function run(argv, io = {}) {
2526
2572
  const { positionals, values, harbor } = parse(
2527
2573
  spec(
2528
2574
  ["confirm", "over-limit"],
2529
- ["title", "assignee", "due", "priority", "status", "notes", "repeat", "waits-on"],
2575
+ [
2576
+ "title",
2577
+ "assignee",
2578
+ "also",
2579
+ "due",
2580
+ "priority",
2581
+ "status",
2582
+ "notes",
2583
+ "repeat",
2584
+ "waits-on",
2585
+ ],
2530
2586
  ),
2531
2587
  );
2532
2588
  const waits = values["waits-on"];
@@ -2537,6 +2593,7 @@ export async function run(argv, io = {}) {
2537
2593
  task: one(positionals, "task key, like RR-42"),
2538
2594
  title: values.title,
2539
2595
  assignee: values.assignee,
2596
+ also: values.also === undefined ? undefined : alsoList(values.also),
2540
2597
  due:
2541
2598
  values.due === undefined || duePilot(values.due)
2542
2599
  ? undefined
@@ -2561,6 +2618,11 @@ export async function run(argv, io = {}) {
2561
2618
  (r) =>
2562
2619
  [
2563
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,
2564
2626
  r.updated.waitsOn ? `Waits on ${r.updated.waitsOn.join(", ")}.` : null,
2565
2627
  r.updated.repeat ? `Repeats ${REPEAT_WORD[r.updated.repeat]}.` : null,
2566
2628
  ...(values.due && !duePilot(values.due) ? pilotLines(r, r.updated) : []),
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 : (t.assignee ?? 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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.72.0",
3
+ "version": "0.73.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",