@harborppm/cli 0.50.0 → 0.51.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 CHANGED
@@ -155,9 +155,11 @@ harbor risk "TITLE" --project P|--program P [--impact 1-5] [--probability 1-5]
155
155
  harbor issue "TITLE" --project P|--program P [--impact 1-5] [--owner NAME]
156
156
  [--due DATE]
157
157
  harbor decision "TITLE" --project P|--program P [--owner NAME] [--due DATE]
158
+ [--turned-down T] [--look-again DATE] [--supersedes "OLD"]
158
159
  harbor risks [words] [--kind risk,issue,decision] [--project P] [--closed]
159
160
  harbor raid "TITLE" [--status S] [--owner me|none|NAME] [--impact 1-5] [--probability 1-5]
160
- [--due DATE|none] [--update TEXT] [--mitigation T] [--resolution T] …
161
+ [--due DATE|none] [--update TEXT] [--mitigation T] [--resolution T]
162
+ [--turned-down T] [--look-again DATE|none] …
161
163
  one risk, issue or decision in full, or change it
162
164
  harbor review [WORDS] [--mine|--owner NAME] [--portfolio P] [--done]
163
165
  the review: programs worst first, with next moves
package/dist/app.mjs CHANGED
@@ -2069,7 +2069,8 @@ function normalizeRaidItem(r) {
2069
2069
  updates: Array.isArray(r.updates) ? r.updates : [],
2070
2070
  scoreHistory: Array.isArray(r.scoreHistory) && r.scoreHistory.length ? r.scoreHistory : void 0,
2071
2071
  ...kind === "risk" && typeof r.happened === "boolean" ? { happened: r.happened } : {},
2072
- ...kind === "decision" ? decisionFields(r) : {}
2072
+ ...kind === "decision" ? decisionFields(r) : {},
2073
+ ...ids$1(r.followerIds).length ? { followerIds: ids$1(r.followerIds) } : {}
2073
2074
  };
2074
2075
  }
2075
2076
  /** A revisit condition that still says something, or null. */
@@ -14719,6 +14720,24 @@ var init_assignment = __esmMin((() => {}));
14719
14720
  //#region src/lib/slices/tasks.ts
14720
14721
  function tasksSlice({ set, get, write }) {
14721
14722
  return {
14723
+ followWork: (personId, taskIds, raidIds = []) => {
14724
+ const tasks = new Set(taskIds);
14725
+ const raid = new Set(raidIds);
14726
+ const add = (ids) => ids?.includes(personId) ? ids : [...ids ?? [], personId];
14727
+ write({
14728
+ tasks: get().tasks.map((t) => tasks.has(t.id) && t.assigneeId !== personId ? {
14729
+ ...t,
14730
+ followerIds: add(t.followerIds)
14731
+ } : t),
14732
+ raidItems: get().raidItems.map((r) => raid.has(r.id) && r.ownerId !== personId ? {
14733
+ ...r,
14734
+ followerIds: add(r.followerIds),
14735
+ updatedAt: stamp()
14736
+ } : r)
14737
+ });
14738
+ const who = get().people.find((p) => p.id === personId);
14739
+ get().log(`added ${who?.name ?? "someone"} as a second pair of hands on ${taskIds.length + raidIds.length} item${taskIds.length + raidIds.length === 1 ? "" : "s"}`, "/people");
14740
+ },
14722
14741
  addTask: (t, opts) => {
14723
14742
  if (!opts?.allowOverCap) {
14724
14743
  if (columnWip(get(), t.projectId, t.status, 1)) return "";
package/lib/cli.mjs CHANGED
@@ -142,6 +142,8 @@ export const HELP = `harbor: Harbor from the terminal
142
142
  harbor issue "TITLE" --project P|--program P [--impact 1-5] [--owner NAME]
143
143
  [--due DATE]
144
144
  harbor decision "TITLE" --project P|--program P [--owner NAME] [--due DATE]
145
+ [--turned-down T] [--look-again DATE]
146
+ [--supersedes "OLD DECISION"]
145
147
  harbor risks [words] [--kind risk,issue,decision] [--project P] [--closed]
146
148
  the register, worst first
147
149
  (--closed adds closed ones)
@@ -149,7 +151,7 @@ export const HELP = `harbor: Harbor from the terminal
149
151
  [--probability 1-5] [--due DATE|none] [--update TEXT]
150
152
  [--title T] [--description T] [--mitigation T]
151
153
  [--contingency T] [--resolution T] [--rationale T]
152
- [--notes T]
154
+ [--notes T] [--turned-down T] [--look-again DATE|none]
153
155
  one risk, issue or decision in
154
156
  full, or change it
155
157
  harbor review [WORDS] [--mine|--owner NAME] [--portfolio P] [--done]
@@ -743,7 +745,8 @@ const DETAIL = {
743
745
  " Also --owner me|none|NAME, --impact 1-5, --probability 1-5 (risks),",
744
746
  " --due DATE|none (review by, or decide by), --title, --description,",
745
747
  " --mitigation and --contingency (risks), --resolution (issues and",
746
- " decisions), --rationale (decisions) and --notes. TITLE is its title or",
748
+ " decisions), --rationale, --turned-down and --look-again DATE|none",
749
+ " (decisions) and --notes. TITLE is its title or",
747
750
  " enough of it to pick one out; harbor risks lists them. Changes several",
748
751
  " at once, as one change.",
749
752
  ],
@@ -794,6 +797,12 @@ const DETAIL = {
794
797
  ' harbor decision "TITLE" --project P writes: logs a decision to make',
795
798
  ' harbor decision "TITLE" --program P writes: logs one on the program',
796
799
  " [--owner NAME] [--due DATE] (the date to decide by)",
800
+ " [--turned-down T] (what was turned down, and why not)",
801
+ " [--look-again DATE] (when it comes back to the top of the brief)",
802
+ ' harbor decision "TITLE" --supersedes "OLD"',
803
+ " writes: a decision that replaces an",
804
+ " earlier one, which reads as",
805
+ " superseded",
797
806
  ],
798
807
  };
799
808
 
@@ -850,6 +859,9 @@ function distance(a, b) {
850
859
  /** A mistyped command, with the one it was probably meant to be. */
851
860
  export function noCommand(name) {
852
861
  if (name.startsWith("-")) return `${name} goes after a command, like harbor mine ${name}.`;
862
+ // A plural kind reads the register, not the command that writes one.
863
+ if (["decisions", "issues"].includes(name))
864
+ return `No command "${name}". harbor risks --kind ${name.slice(0, -1)} lists them.`;
853
865
  const near = COMMANDS.filter((c) => distance(name, c) <= (c.length > 4 ? 2 : 1));
854
866
  const guess = near.length === 1 ? ` Did you mean harbor ${near[0]}?` : "";
855
867
  return `No command "${name}".${guess} Run harbor help.`;
@@ -1963,7 +1975,7 @@ export async function run(argv, io = {}) {
1963
1975
  includeClosed: Boolean(values.closed),
1964
1976
  limit: 50,
1965
1977
  },
1966
- (r) => raidList(r, positionals.join(" ")),
1978
+ (r) => raidList(r, positionals.join(" "), kind),
1967
1979
  );
1968
1980
  }
1969
1981
  case "report": {
@@ -2292,6 +2304,8 @@ export async function run(argv, io = {}) {
2292
2304
  "update",
2293
2305
  "decided-by",
2294
2306
  "decided-on",
2307
+ "turned-down",
2308
+ "look-again",
2295
2309
  ...fields,
2296
2310
  ],
2297
2311
  ),
@@ -2309,6 +2323,9 @@ export async function run(argv, io = {}) {
2309
2323
  decidedBy: values["decided-by"],
2310
2324
  decidedOn:
2311
2325
  values["decided-on"] === undefined ? undefined : nullable(dueDate(values["decided-on"])),
2326
+ turnedDown: values["turned-down"],
2327
+ revisitOn:
2328
+ values["look-again"] === undefined ? undefined : nullable(dueDate(values["look-again"])),
2312
2329
  update: values.update,
2313
2330
  ...Object.fromEntries(fields.map((f) => [f, values[f]])),
2314
2331
  };
@@ -2370,11 +2387,15 @@ export async function run(argv, io = {}) {
2370
2387
  spec(
2371
2388
  [],
2372
2389
  ["project", "program", "owner", "due"].concat(
2373
- command === "risk" ? ["impact", "probability"] : command === "issue" ? ["impact"] : [],
2390
+ command === "risk"
2391
+ ? ["impact", "probability"]
2392
+ : command === "issue"
2393
+ ? ["impact"]
2394
+ : ["supersedes", "turned-down", "look-again"],
2374
2395
  ),
2375
2396
  ),
2376
2397
  );
2377
- if (!values.project && !values.program) {
2398
+ if (!values.project && !values.program && !values.supersedes) {
2378
2399
  throw new HarborError("Say where it belongs: --project NAME or --program NAME.");
2379
2400
  }
2380
2401
  return call(
@@ -2389,8 +2410,14 @@ export async function run(argv, io = {}) {
2389
2410
  impact: level(values.impact, "impact"),
2390
2411
  probability: level(values.probability, "probability"),
2391
2412
  due: dueDate(values.due),
2413
+ supersedes: values.supersedes,
2414
+ turnedDown: values["turned-down"],
2415
+ revisitOn: values["look-again"] === undefined ? undefined : dueDate(values["look-again"]),
2392
2416
  },
2393
- (r) => `Logged the ${command} “${r.raised.title}”.`,
2417
+ (r) =>
2418
+ r.replaces
2419
+ ? `Logged the decision “${r.raised.title}”; it replaces “${r.replaces.title}”.`
2420
+ : `Logged the ${command} “${r.raised.title}”.`,
2394
2421
  );
2395
2422
  }
2396
2423
  default:
package/lib/format.mjs CHANGED
@@ -246,12 +246,19 @@ export function todayLocal(d = new Date()) {
246
246
  }
247
247
 
248
248
  /** The register, worst first: kind, title, then status, scores, owner and where it sits. */
249
- export function raidList(r, words = "") {
249
+ export function raidList(r, words = "", kinds = []) {
250
250
  const want = words.trim().toLowerCase();
251
251
  const items = (r.items ?? []).filter(
252
252
  (x) => !want || `${x.title} ${x.description ?? ""}`.toLowerCase().includes(want),
253
253
  );
254
- if (!items.length) return want ? "Nothing in the register matches." : "The register is clear.";
254
+ if (!items.length) {
255
+ if (want) return "Nothing in the register matches.";
256
+ const only =
257
+ kinds?.length === 1
258
+ ? { risk: "risks", issue: "issues", decision: "decisions" }[kinds[0]]
259
+ : null;
260
+ return only ? `No ${only} in the register.` : "The register is clear.";
261
+ }
255
262
  const lines = items.map((x) => {
256
263
  const bits = [
257
264
  x.status?.toLowerCase(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.50.0",
3
+ "version": "0.51.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",