@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.
- package/dist/app.mjs +123 -39
- package/lib/cli.mjs +1 -1
- 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
|
|
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()
|
|
5786
|
+
const q = fold(query.trim());
|
|
5740
5787
|
if (!q) return SCORE.anywhere;
|
|
5741
5788
|
const tokens = tokensOf(q);
|
|
5742
|
-
const n = name
|
|
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(
|
|
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")
|
|
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
|
-
|
|
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,
|
|
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")
|
|
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
|
|
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
|
|
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.`
|