@harborppm/cli 0.85.0 → 0.86.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.
Files changed (3) hide show
  1. package/dist/app.mjs +123 -39
  2. package/lib/cli.mjs +1 -1
  3. package/package.json +1 -1
package/dist/app.mjs CHANGED
@@ -5266,6 +5266,49 @@ function cascadeSummary(counts, kind) {
5266
5266
  if (list.length === 1) return list[0];
5267
5267
  return `${list.slice(0, -1).join(", ")} and ${list.at(-1)}`;
5268
5268
  }
5269
+ /** `data` with the cut links put back, wherever both ends exist again. */
5270
+ function relink(data, links) {
5271
+ const out = {};
5272
+ const exists = (key) => new Set(data[key].map((r) => r.id));
5273
+ const targetOf = {
5274
+ programs: "programs",
5275
+ projects: "projects",
5276
+ tasks: "tasks",
5277
+ milestones: "projects"
5278
+ };
5279
+ for (const key of [
5280
+ "programs",
5281
+ "projects",
5282
+ "tasks",
5283
+ "milestones"
5284
+ ]) {
5285
+ const cut = links.blockers[key];
5286
+ if (!cut) continue;
5287
+ const there = exists(targetOf[key]);
5288
+ out[key] = data[key].map((row) => {
5289
+ const was = cut[row.id];
5290
+ if (!was) return row;
5291
+ const ids = [.../* @__PURE__ */ new Set([...was, ...blockerIds(row)])].filter((id) => there.has(id));
5292
+ return ids.length === blockerIds(row).length ? row : {
5293
+ ...row,
5294
+ ...blockersPatch(ids)
5295
+ };
5296
+ });
5297
+ }
5298
+ if (Object.keys(links.focus).length) {
5299
+ const tasks = exists("tasks");
5300
+ out.ropes = (data.ropes ?? []).map((r) => {
5301
+ const lost = links.focus[`${r.personId}|${r.date}`];
5302
+ if (!lost) return r;
5303
+ const back = lost.filter((id) => tasks.has(id) && !r.taskIds.includes(id));
5304
+ return back.length ? {
5305
+ ...r,
5306
+ taskIds: [...r.taskIds, ...back]
5307
+ } : r;
5308
+ });
5309
+ }
5310
+ return out;
5311
+ }
5269
5312
  var init_cascade = __esmMin((() => {
5270
5313
  init_blockers();
5271
5314
  }));
@@ -5731,15 +5774,19 @@ function roadmapDoneCount(data, portfolioId) {
5731
5774
  }
5732
5775
 
5733
5776
  // src/lib/search.ts
5777
+ /** Lower case without accents, so "cafe" finds "Café" and "Zoë" finds "Zoe". */
5778
+ function fold(text) {
5779
+ return text.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
5780
+ }
5734
5781
  function tokensOf(query) {
5735
- return query.toLowerCase().split(/\s+/).filter(Boolean);
5782
+ return fold(query).split(/\s+/).filter(Boolean);
5736
5783
  }
5737
5784
  /** Score a thing by its own name first, then by everything else it carries. */
5738
5785
  function scoreMatch(query, name, ...secondary) {
5739
- const q = query.trim().toLowerCase();
5786
+ const q = fold(query.trim());
5740
5787
  if (!q) return SCORE.anywhere;
5741
5788
  const tokens = tokensOf(q);
5742
- const n = name.toLowerCase();
5789
+ const n = fold(name);
5743
5790
  if (n === q) return SCORE.exact;
5744
5791
  if (n.startsWith(q)) return SCORE.prefix;
5745
5792
  const words = n.split(/\s+/);
@@ -5747,7 +5794,7 @@ function scoreMatch(query, name, ...secondary) {
5747
5794
  const bonus = tokens.every((t) => words.some((w) => w.startsWith(t))) ? 5 : 0;
5748
5795
  return SCORE.name + bonus;
5749
5796
  }
5750
- const blob = [n, ...secondary.filter((s) => Boolean(s)).map((s) => s.toLowerCase())].join(" ");
5797
+ const blob = [n, ...secondary.filter((s) => Boolean(s)).map(fold)].join(" ");
5751
5798
  return tokens.every((t) => blob.includes(t)) ? SCORE.anywhere : 0;
5752
5799
  }
5753
5800
  /** Keep what matches, best first. Ties keep the caller's order. */
@@ -21202,12 +21249,13 @@ const HARBOR_TOOLS = [
21202
21249
  if (input.query) {
21203
21250
  const q = input.query;
21204
21251
  const named = findByKey(data, q);
21252
+ const names = new Map(data.projects.map((p) => [p.id, p.name]));
21205
21253
  list = list.map((t) => ({
21206
21254
  t,
21207
- s: scoreMatch(q, t.title, t.description)
21255
+ s: scoreMatch(q, t.title, t.description, names.get(t.projectId))
21208
21256
  })).filter((h) => h.s > 0 && h.t !== named).sort((a, b) => b.s - a.s).map((h) => h.t);
21209
21257
  if (named) list.unshift(named);
21210
- if (!input.status?.length) doneMatches = data.tasks.filter((t) => t.status === "done" && t !== named && fits(t) && scoreMatch(q, t.title, t.description) > 0).length;
21258
+ if (!input.status?.length) doneMatches = data.tasks.filter((t) => t.status === "done" && t !== named && fits(t) && scoreMatch(q, t.title, t.description, names.get(t.projectId)) > 0).length;
21211
21259
  } else list = [...list].sort((a, b) => a.status === "done" && b.status === "done" ? (b.completedAt ?? "").localeCompare(a.completedAt ?? "") : Number(a.status === "done") - Number(b.status === "done") || (a.dueDate ?? "9999").localeCompare(b.dueDate ?? "9999"));
21212
21260
  return {
21213
21261
  ...doneMatches ? { doneMatches } : {},
@@ -22878,7 +22926,10 @@ function withStatus(task, status) {
22878
22926
  touchedAt: stamp()
22879
22927
  };
22880
22928
  if (status === "done") next.completedAt = stamp();
22881
- else if (task.status === "done") next.completedAt = null;
22929
+ else if (task.status === "done") {
22930
+ next.completedAt = null;
22931
+ if (task.approval?.status === "approved") next.approval = null;
22932
+ }
22882
22933
  return next;
22883
22934
  }
22884
22935
  var init_shared = __esmMin((() => {}));
@@ -23051,6 +23102,26 @@ var init_session = __esmMin((() => {
23051
23102
  init_format();
23052
23103
  }));
23053
23104
 
23105
+ // src/lib/assignment.ts
23106
+ /**
23107
+ * Record who set a task's assignee and when. Only a change of assignee
23108
+ * stamps; clearing the assignee clears the stamp.
23109
+ */
23110
+ function stampAssignment(before, next, by, at) {
23111
+ if (before && before.assigneeId === next.assigneeId) return next;
23112
+ if (!next.assigneeId) return {
23113
+ ...next,
23114
+ assignedBy: null,
23115
+ assignedAt: null
23116
+ };
23117
+ return {
23118
+ ...next,
23119
+ assignedBy: by,
23120
+ assignedAt: at
23121
+ };
23122
+ }
23123
+ var init_assignment = __esmMin((() => {}));
23124
+
23054
23125
  // src/lib/first-week.ts
23055
23126
  /** The program launch named for you, when nobody has renamed it yet. */
23056
23127
  function starterProgram(data) {
@@ -23377,13 +23448,13 @@ function hierarchySlice({ get, write }) {
23377
23448
  const program = get().programs.find((p) => p.id === programId);
23378
23449
  get().log(`moved ${ids.length} project${ids.length === 1 ? "" : "s"} to ${program?.name ?? "a program"}`, `/programs/${programId}`);
23379
23450
  },
23380
- restoreRows: (rows) => {
23451
+ restoreRows: (rows, links) => {
23381
23452
  const state = get();
23382
23453
  const back = (current, add) => {
23383
23454
  const have = new Set((current ?? []).map((r) => r.id));
23384
23455
  return [...current ?? [], ...(add ?? []).filter((r) => !have.has(r.id))];
23385
23456
  };
23386
- write({
23457
+ const next = {
23387
23458
  portfolios: back(state.portfolios, rows.portfolios),
23388
23459
  programs: back(state.programs, rows.programs),
23389
23460
  projects: back(state.projects, rows.projects),
@@ -23399,7 +23470,14 @@ function hierarchySlice({ get, write }) {
23399
23470
  ...rows.boardConfigs ?? {},
23400
23471
  ...state.boardConfigs ?? {}
23401
23472
  }
23402
- });
23473
+ };
23474
+ write(links ? {
23475
+ ...next,
23476
+ ...relink({
23477
+ ...state,
23478
+ ...next
23479
+ }, links)
23480
+ } : next);
23403
23481
  const name = rows.portfolios?.[0]?.name ?? rows.programs?.[0]?.name ?? rows.projects?.[0]?.name;
23404
23482
  if (name) get().log(`put back “${name}”`);
23405
23483
  },
@@ -23410,10 +23488,13 @@ function hierarchySlice({ get, write }) {
23410
23488
  const ids = have(current);
23411
23489
  return [...current, ...(more ?? []).filter((r) => !ids.has(r.id))];
23412
23490
  };
23491
+ const small = (rows.tasks?.length ?? 0) <= 20;
23492
+ const at = stamp();
23493
+ const incoming = small ? rows.tasks?.map((t) => stampAssignment(void 0, t, state.currentUserId, at)) : rows.tasks;
23413
23494
  write({
23414
23495
  programs: add(state.programs, rows.programs),
23415
23496
  projects: add(state.projects, rows.projects),
23416
- tasks: add(state.tasks, rows.tasks),
23497
+ tasks: add(state.tasks, incoming),
23417
23498
  people: add(state.people, rows.people),
23418
23499
  raidItems: add(state.raidItems, rows.raidItems)
23419
23500
  });
@@ -23484,6 +23565,7 @@ function hierarchySlice({ get, write }) {
23484
23565
  }
23485
23566
  };
23486
23567
  }
23568
+ var SMALL_IMPORT;
23487
23569
  var init_hierarchy = __esmMin((() => {
23488
23570
  init_board();
23489
23571
  init_cascade();
@@ -23493,11 +23575,13 @@ var init_hierarchy = __esmMin((() => {
23493
23575
  init_health_history();
23494
23576
  init_derived_health();
23495
23577
  init_shared();
23578
+ init_assignment();
23496
23579
  init_hill();
23497
23580
  init_charter_lines();
23498
23581
  init_format();
23499
23582
  init_empty();
23500
23583
  init_first_week();
23584
+ SMALL_IMPORT = 20;
23501
23585
  }));
23502
23586
 
23503
23587
  // src/lib/github-refs.ts
@@ -23546,6 +23630,16 @@ function runActions(actions, start, ctx) {
23546
23630
  for (const action of actions) switch (action.type) {
23547
23631
  case "set_status": {
23548
23632
  if (!action.status) break;
23633
+ if (action.status === "done" && task.status !== "done" && ctx.waiting?.(task)) {
23634
+ comments.push({
23635
+ taskId: task.id,
23636
+ authorId: ctx.authorId,
23637
+ body: "Left open: this still waits on unfinished work, so the rule didn’t finish it.",
23638
+ createdAt: ctx.now,
23639
+ mentionedIds: []
23640
+ });
23641
+ break;
23642
+ }
23549
23643
  const wasDone = task.status === "done";
23550
23644
  const completedAt = action.status === "done" ? task.completedAt ?? ctx.now : wasDone ? null : task.completedAt;
23551
23645
  task = {
@@ -23611,7 +23705,8 @@ function applyAutomations(project, prev, next, opts) {
23611
23705
  const ctx = {
23612
23706
  authorId: opts.currentUserId,
23613
23707
  now,
23614
- today
23708
+ today,
23709
+ waiting: opts.waiting
23615
23710
  };
23616
23711
  let task = next;
23617
23712
  const comments = [];
@@ -23717,26 +23812,6 @@ var init_recurrence = __esmMin((() => {
23717
23812
  step = (d, repeat, n) => repeat === "weekly" ? addDays(d, 7 * n) : repeat === "biweekly" ? addDays(d, 14 * n) : addMonths$1(d, n);
23718
23813
  }));
23719
23814
 
23720
- // src/lib/assignment.ts
23721
- /**
23722
- * Record who set a task's assignee and when. Only a change of assignee
23723
- * stamps; clearing the assignee clears the stamp.
23724
- */
23725
- function stampAssignment(before, next, by, at) {
23726
- if (before && before.assigneeId === next.assigneeId) return next;
23727
- if (!next.assigneeId) return {
23728
- ...next,
23729
- assignedBy: null,
23730
- assignedAt: null
23731
- };
23732
- return {
23733
- ...next,
23734
- assignedBy: by,
23735
- assignedAt: at
23736
- };
23737
- }
23738
- var init_assignment = __esmMin((() => {}));
23739
-
23740
23815
  // src/lib/bulk.ts
23741
23816
  /** A task's due date moved by `days`, or null when it has none. */
23742
23817
  function shiftedDue(due, days) {
@@ -23833,7 +23908,8 @@ function tasksSlice({ set, get, write }) {
23833
23908
  const project = get().projects.find((p) => p.id === t.projectId);
23834
23909
  const { task: automated, comments: autoComments } = applyAutomations(project, void 0, row, {
23835
23910
  created: true,
23836
- currentUserId: get().currentUserId
23911
+ currentUserId: get().currentUserId,
23912
+ waiting: (t) => isBlocked$1(t, get().tasks)
23837
23913
  });
23838
23914
  const finalRow = stampAssignment(void 0, automated, get().currentUserId, stamp());
23839
23915
  write({
@@ -23876,12 +23952,16 @@ function tasksSlice({ set, get, write }) {
23876
23952
  if (next.assigneeId && next.followerIds?.includes(next.assigneeId)) next.followerIds = alsoOnIds(next);
23877
23953
  if (patch.status && patch.status !== row.status) {
23878
23954
  if (patch.status === "done" && !patch.completedAt) next.completedAt = stamp();
23879
- else if (row.status === "done" && patch.status !== "done") next.completedAt = null;
23955
+ else if (row.status === "done" && patch.status !== "done") {
23956
+ next.completedAt = null;
23957
+ if (row.approval?.status === "approved" && !("approval" in patch)) next.approval = null;
23958
+ }
23880
23959
  if (patch.status === "done" && isBlocked$1(row, get().tasks)) Object.assign(next, blockersPatch([]));
23881
23960
  }
23882
23961
  const { task: finalTask, comments: autoComments } = applyAutomations(project, row, next, {
23883
23962
  created: false,
23884
- currentUserId: get().currentUserId
23963
+ currentUserId: get().currentUserId,
23964
+ waiting: (t) => isBlocked$1(t, get().tasks)
23885
23965
  });
23886
23966
  newComments.push(...autoComments.map((c) => ({
23887
23967
  ...c,
@@ -23920,7 +24000,8 @@ function tasksSlice({ set, get, write }) {
23920
24000
  const project = get().projects.find((p) => p.id === task.projectId);
23921
24001
  const { task: finalRow, comments: autoComments } = applyAutomations(project, task, nextRow, {
23922
24002
  created: false,
23923
- currentUserId: get().currentUserId
24003
+ currentUserId: get().currentUserId,
24004
+ waiting: (t) => isBlocked$1(t, get().tasks)
23924
24005
  });
23925
24006
  const follow = status === "done" ? nextOccurrence(finalRow, uid("t"), todayISO(), stamp(), tasksOf(get().tasks, task.projectId)) : null;
23926
24007
  const moved = get().tasks.map((row) => row.id === id ? finalRow : row);
@@ -24002,7 +24083,8 @@ function tasksSlice({ set, get, write }) {
24002
24083
  },
24003
24084
  updateComment: (id, body) => {
24004
24085
  const comment = get().comments.find((c) => c.id === id);
24005
- if (!comment || comment.authorId !== get().currentUserId) return;
24086
+ if (!comment) return;
24087
+ if (comment.authorId !== get().currentUserId && get().myRole !== "owner") return;
24006
24088
  const trimmed = body.trim();
24007
24089
  if (!trimmed) return;
24008
24090
  const mentionedIds = parseMentions(trimmed, get().people).filter((pid) => pid !== get().currentUserId);
@@ -24015,7 +24097,8 @@ function tasksSlice({ set, get, write }) {
24015
24097
  },
24016
24098
  deleteComment: (id) => {
24017
24099
  const comment = get().comments.find((c) => c.id === id);
24018
- if (!comment || comment.authorId !== get().currentUserId) return;
24100
+ if (!comment) return;
24101
+ if (comment.authorId !== get().currentUserId && get().myRole !== "owner") return;
24019
24102
  set({ comments: get().comments.filter((c) => c.id !== id) });
24020
24103
  },
24021
24104
  addChecklistItem: (taskId, title) => {
@@ -24196,7 +24279,8 @@ function tasksSlice({ set, get, write }) {
24196
24279
  }
24197
24280
  const { task: automated, comments } = applyAutomations(projects.get(row.projectId), row, next, {
24198
24281
  created: false,
24199
- currentUserId: me
24282
+ currentUserId: me,
24283
+ waiting: (t) => isBlocked$1(t, s.tasks)
24200
24284
  });
24201
24285
  newComments.push(...comments.map((c) => ({
24202
24286
  ...c,
package/lib/cli.mjs CHANGED
@@ -2737,7 +2737,7 @@ export async function run(argv, io = {}) {
2737
2737
  },
2738
2738
  (r) =>
2739
2739
  [
2740
- `${r.updated.title}: ${{ doing: "started", review: "in review", done: "done" }[status]}.`,
2740
+ `${r.updated.title}: ${r.reopened ? "reopened, " : ""}${{ doing: "started", review: "in review", done: "done" }[status]}.`,
2741
2741
  r.queuedNext ? `Next one: ${taskLine(r.queuedNext, opts)}` : "",
2742
2742
  r.projectFinished
2743
2743
  ? `That was the last open task in ${r.projectFinished}. Mark the project Done in the app (harbor) or on the website.`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.85.0",
3
+ "version": "0.86.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",