@harborppm/cli 0.19.0 → 0.20.1
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 +3 -0
- package/dist/app.mjs +26 -7
- package/lib/cli.mjs +43 -2
- package/lib/format.mjs +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,8 +118,11 @@ harbor decision "TITLE" --project P|--program P [--owner NAME] [--due DATE]
|
|
|
118
118
|
harbor risks [words] [--kind risk,issue,decision] [--project P] [--closed]
|
|
119
119
|
harbor premortem PROJECT ["WHAT WENT WRONG"] [--start]
|
|
120
120
|
harbor voyage PROJECT ["YOUR LINE"]
|
|
121
|
+
harbor limit PROJECT [COLUMN N|none]
|
|
121
122
|
```
|
|
122
123
|
|
|
124
|
+
`harbor limit RR` prints how many tasks each of a board's columns takes at once; `harbor limit RR doing 8` changes one (1 to 20, or `none` for no limit), if you own the harbor. `harbor projects --json` carries each board's `limits` too.
|
|
125
|
+
|
|
123
126
|
`harbor premortem` runs a pre-mortem with your crew: `--start` asks everyone "it's after the end date and this project failed: what happened?", each person adds answers alone for five minutes, and then the answers show together on the project's Risks & issues tab, where each becomes a risk in one click. On its own it prints the question and the answers you can see. Once a project is Done, `harbor voyage` prints its voyage log (planned against actual dates, the biggest slips, the risks that came true, who closed the most, and the crew's lines) and adds yours.
|
|
124
127
|
|
|
125
128
|
Every command takes `--json` for the raw result (a refusal then comes out as `{"error": "…"}` on standard output, still with exit code 1), and `harbor COMMAND --help` shows just that command. Statuses are backlog, todo, doing, review and done. A `DATE` is YYYY-MM-DD, `today`, `tomorrow`, or `+N` for N days from today.
|
package/dist/app.mjs
CHANGED
|
@@ -1264,6 +1264,18 @@ const ALL_FIELDS = {
|
|
|
1264
1264
|
};
|
|
1265
1265
|
const DEFAULT_DOING_WIP = 3;
|
|
1266
1266
|
const DEFAULT_REVIEW_WIP = 2;
|
|
1267
|
+
/** The column's cap after "No cap" (`uncapped`) or a number is picked. */
|
|
1268
|
+
function withWipLimit(col, limit) {
|
|
1269
|
+
const { uncapped: _drop, ...rest } = col;
|
|
1270
|
+
return limit == null ? {
|
|
1271
|
+
...rest,
|
|
1272
|
+
wipLimit: null,
|
|
1273
|
+
uncapped: true
|
|
1274
|
+
} : {
|
|
1275
|
+
...rest,
|
|
1276
|
+
wipLimit: limit
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1267
1279
|
function defaultWipFor(status) {
|
|
1268
1280
|
if (status === "doing") return 3;
|
|
1269
1281
|
if (status === "review") return 2;
|
|
@@ -1337,6 +1349,15 @@ function clampWip(value) {
|
|
|
1337
1349
|
if (!Number.isFinite(n) || n <= 0) return null;
|
|
1338
1350
|
return Math.min(20, Math.round(n));
|
|
1339
1351
|
}
|
|
1352
|
+
function capOf(c) {
|
|
1353
|
+
const n = clampWip(c.wipLimit);
|
|
1354
|
+
if (n != null) return { wipLimit: n };
|
|
1355
|
+
if (c.uncapped === true) return {
|
|
1356
|
+
wipLimit: null,
|
|
1357
|
+
uncapped: true
|
|
1358
|
+
};
|
|
1359
|
+
return { wipLimit: defaultWipFor(c.status) };
|
|
1360
|
+
}
|
|
1340
1361
|
function normalizeBoardConfig(raw) {
|
|
1341
1362
|
const base = defaultBoardConfig();
|
|
1342
1363
|
if (!raw) return base;
|
|
@@ -1350,7 +1371,7 @@ function normalizeBoardConfig(raw) {
|
|
|
1350
1371
|
status: c.status,
|
|
1351
1372
|
label: typeof c.label === "string" && c.label.trim() ? c.label.trim() : TASK_STATUS_LABEL[c.status],
|
|
1352
1373
|
visible: c.visible !== false,
|
|
1353
|
-
|
|
1374
|
+
...capOf(c)
|
|
1354
1375
|
});
|
|
1355
1376
|
}
|
|
1356
1377
|
for (const status of TASK_COLUMNS) if (!order.includes(status)) order.push(status);
|
|
@@ -7532,8 +7553,9 @@ function daysUntil(date, now) {
|
|
|
7532
7553
|
function incomplete(task) {
|
|
7533
7554
|
return task.status !== "done";
|
|
7534
7555
|
}
|
|
7535
|
-
function wipLimitOf(status, stored) {
|
|
7556
|
+
function wipLimitOf(status, stored, uncapped) {
|
|
7536
7557
|
if (stored != null) return stored;
|
|
7558
|
+
if (uncapped === true) return null;
|
|
7537
7559
|
if (status === "doing") return 3;
|
|
7538
7560
|
if (status === "review") return 2;
|
|
7539
7561
|
return null;
|
|
@@ -7542,7 +7564,7 @@ function wipOverflowing(data, projectId) {
|
|
|
7542
7564
|
const cols = data.boardConfigs?.[projectId]?.columns;
|
|
7543
7565
|
if (cols?.length) return cols.some((c) => {
|
|
7544
7566
|
if (c.visible === false) return false;
|
|
7545
|
-
const limit = wipLimitOf(c.status, c.wipLimit);
|
|
7567
|
+
const limit = wipLimitOf(c.status, c.wipLimit, c.uncapped);
|
|
7546
7568
|
if (limit == null) return false;
|
|
7547
7569
|
return tasksOf(data.tasks, projectId).filter((t) => t.status === c.status).length > limit;
|
|
7548
7570
|
});
|
|
@@ -8704,10 +8726,7 @@ function hierarchySlice({ get, write }) {
|
|
|
8704
8726
|
const columns = config.columns.map((c) => {
|
|
8705
8727
|
if (c.status !== status) return c;
|
|
8706
8728
|
const floor = Math.max(c.wipLimit ?? 0, current);
|
|
8707
|
-
return
|
|
8708
|
-
...c,
|
|
8709
|
-
wipLimit: Math.min(20, floor + 1)
|
|
8710
|
-
};
|
|
8729
|
+
return withWipLimit(c, Math.min(20, floor + 1));
|
|
8711
8730
|
});
|
|
8712
8731
|
get().setBoardConfig(projectId, {
|
|
8713
8732
|
...config,
|
package/lib/cli.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import {
|
|
20
20
|
STATUS,
|
|
21
21
|
harbors,
|
|
22
|
+
limitsText,
|
|
22
23
|
myWork,
|
|
23
24
|
premortemText,
|
|
24
25
|
voyageText,
|
|
@@ -59,6 +60,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
59
60
|
harbor programs [--done] programs and their projects
|
|
60
61
|
harbor projects [words] [--program P] [--done]
|
|
61
62
|
projects with health and open work
|
|
63
|
+
(--json adds each board's limits)
|
|
62
64
|
harbor mine what's on you, late first
|
|
63
65
|
harbor tasks [words] [--project P] [--assignee me|none|NAME]
|
|
64
66
|
[--late] [--status S,S]
|
|
@@ -83,6 +85,8 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
83
85
|
[--priority P] [--status S] [--notes TEXT]
|
|
84
86
|
[--repeat weekly|biweekly|monthly|never]
|
|
85
87
|
[--waits-on TASK,TASK|none] [--confirm] [--over-limit]
|
|
88
|
+
harbor limit PROJECT [COLUMN N|none] how many tasks a board column
|
|
89
|
+
takes at once; the owner sets it
|
|
86
90
|
harbor comment TASK "TEXT"
|
|
87
91
|
harbor checkin "WHAT YOU'RE DOING" [--on-blocker]
|
|
88
92
|
harbor risk "TITLE" --project P|--program P [--impact 1-5] [--probability 1-5]
|
|
@@ -213,6 +217,8 @@ const FLAG = {
|
|
|
213
217
|
repeat: "--repeat",
|
|
214
218
|
description: "--notes",
|
|
215
219
|
body: "the comment",
|
|
220
|
+
column: "the column",
|
|
221
|
+
limit: "the limit",
|
|
216
222
|
};
|
|
217
223
|
|
|
218
224
|
/** Harbor's refusals are worded for agents; say them as the commands and flags that answer them. */
|
|
@@ -283,6 +289,17 @@ export function premortemForTerminal(r) {
|
|
|
283
289
|
|
|
284
290
|
/** Commands whose forms do different things: one line each, saying which read and which write. */
|
|
285
291
|
const DETAIL = {
|
|
292
|
+
limit: [
|
|
293
|
+
" harbor limit PROJECT reads its board's column limits",
|
|
294
|
+
" harbor limit PROJECT COLUMN N writes: COLUMN takes at most N tasks",
|
|
295
|
+
" at once (1 to 20; the website offers",
|
|
296
|
+
" 1-5, 8, 10 and 15)",
|
|
297
|
+
" harbor limit PROJECT COLUMN none writes: no limit on COLUMN",
|
|
298
|
+
"",
|
|
299
|
+
" COLUMN is a status: doing or review usually, or backlog, todo, done.",
|
|
300
|
+
" Only the harbor's owner can change a limit. A move past a limit is",
|
|
301
|
+
" refused until you run it again with --over-limit.",
|
|
302
|
+
],
|
|
286
303
|
premortem: [
|
|
287
304
|
" harbor premortem PROJECT reads it: the question, the time left,",
|
|
288
305
|
" and the answers you may see",
|
|
@@ -851,13 +868,17 @@ export async function run(argv, io = {}) {
|
|
|
851
868
|
lines.push(
|
|
852
869
|
"",
|
|
853
870
|
"Waits on:",
|
|
854
|
-
...t.waitingOn.map(
|
|
871
|
+
...t.waitingOn.map(
|
|
872
|
+
(w) => ` ${w.key ?? w.id} ${w.title} (${STATUS[w.status] ?? w.status})`,
|
|
873
|
+
),
|
|
855
874
|
);
|
|
856
875
|
if (t.waitedOnBy.length)
|
|
857
876
|
lines.push(
|
|
858
877
|
"",
|
|
859
878
|
"Holds up:",
|
|
860
|
-
...t.waitedOnBy.map(
|
|
879
|
+
...t.waitedOnBy.map(
|
|
880
|
+
(w) => ` ${w.key ?? w.id} ${w.title} (${STATUS[w.status] ?? w.status})`,
|
|
881
|
+
),
|
|
861
882
|
);
|
|
862
883
|
if (t.comments.length)
|
|
863
884
|
lines.push(
|
|
@@ -1111,6 +1132,26 @@ export async function run(argv, io = {}) {
|
|
|
1111
1132
|
}
|
|
1112
1133
|
return call("get_premortem", { harbor, project }, premortemText, premortemForTerminal);
|
|
1113
1134
|
}
|
|
1135
|
+
case "limit": {
|
|
1136
|
+
const { positionals, harbor } = parse(spec());
|
|
1137
|
+
const [project, column, value, ...extra] = positionals;
|
|
1138
|
+
if (!project) throw new HarborError("Give a project, like harbor limit RR doing 5.");
|
|
1139
|
+
if (!column) {
|
|
1140
|
+
return call("get_project", { harbor, project }, (r) =>
|
|
1141
|
+
limitsText(r.name, r.limits, r.tasks),
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
if (value === undefined || extra.length) {
|
|
1145
|
+
throw new HarborError(`Give one limit for ${column}: a number like 5, or none.`);
|
|
1146
|
+
}
|
|
1147
|
+
const limit = /^(none|no|off|0)$/i.test(value) ? null : Number(value);
|
|
1148
|
+
if (limit !== null && !(Number.isInteger(limit) && limit >= 1 && limit <= 20)) {
|
|
1149
|
+
throw new HarborError("A limit is a whole number from 1 to 20, or none.");
|
|
1150
|
+
}
|
|
1151
|
+
return call("set_column_limit", { harbor, project, column, limit }, (r) =>
|
|
1152
|
+
limitsText(r.set.project, r.limits, r.tasks),
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1114
1155
|
case "voyage": {
|
|
1115
1156
|
const { positionals, harbor } = parse(spec());
|
|
1116
1157
|
const [project, ...words] = positionals;
|
package/lib/format.mjs
CHANGED
|
@@ -144,6 +144,18 @@ export function projectList(r, { done = false } = {}) {
|
|
|
144
144
|
);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/** A project's column limits with how full each is: "doing 4 of 5; no limit on backlog, todo, done". */
|
|
148
|
+
export function limitsText(name, limits, tasks = {}) {
|
|
149
|
+
const entries = Object.entries(limits ?? {});
|
|
150
|
+
const capped = entries
|
|
151
|
+
.filter(([, n]) => n != null)
|
|
152
|
+
.map(([status, n]) => `${STATUS[status] ?? status} ${tasks[status] ?? 0} of ${n}`);
|
|
153
|
+
const open = entries.filter(([, n]) => n == null).map(([status]) => STATUS[status] ?? status);
|
|
154
|
+
if (!capped.length) return `${name}'s board has no column limits.`;
|
|
155
|
+
const rest = open.length ? `; no limit on ${open.join(", ")}` : "";
|
|
156
|
+
return `${name}: ${capped.join(", ")}${rest}.`;
|
|
157
|
+
}
|
|
158
|
+
|
|
147
159
|
export function harbors(r) {
|
|
148
160
|
const list = r.harbors ?? [];
|
|
149
161
|
if (!list.length) return "You aren't in any harbor yet. Run harbor to make one.";
|