@harborppm/cli 0.16.0 → 0.18.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 +2 -1
- package/dist/app.mjs +29 -1
- package/lib/cli.mjs +61 -6
- package/lib/format.mjs +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -117,9 +117,10 @@ harbor issue "TITLE" --project P|--program P [--impact 1-5] [--owner NAME]
|
|
|
117
117
|
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
|
+
harbor voyage PROJECT ["YOUR LINE"]
|
|
120
121
|
```
|
|
121
122
|
|
|
122
|
-
`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.
|
|
123
|
+
`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.
|
|
123
124
|
|
|
124
125
|
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.
|
|
125
126
|
|
package/dist/app.mjs
CHANGED
|
@@ -855,7 +855,8 @@ function normalizeRaidItem(r) {
|
|
|
855
855
|
createdAt: r.createdAt ?? now,
|
|
856
856
|
updatedAt: r.updatedAt ?? r.createdAt ?? now,
|
|
857
857
|
updates: Array.isArray(r.updates) ? r.updates : [],
|
|
858
|
-
scoreHistory: Array.isArray(r.scoreHistory) && r.scoreHistory.length ? r.scoreHistory : void 0
|
|
858
|
+
scoreHistory: Array.isArray(r.scoreHistory) && r.scoreHistory.length ? r.scoreHistory : void 0,
|
|
859
|
+
...kind === "risk" && typeof r.happened === "boolean" ? { happened: r.happened } : {}
|
|
859
860
|
};
|
|
860
861
|
}
|
|
861
862
|
/** Active first, then by score, then most recently touched. */
|
|
@@ -9336,6 +9337,33 @@ function milestonesRaidSlice({ set, get, write }) {
|
|
|
9336
9337
|
...r,
|
|
9337
9338
|
skipped
|
|
9338
9339
|
} : r) });
|
|
9340
|
+
},
|
|
9341
|
+
writeVoyageLine: (projectId, body) => {
|
|
9342
|
+
const text = body.trim().slice(0, 600);
|
|
9343
|
+
const me = get().currentUserId;
|
|
9344
|
+
const all = get().reflections ?? [];
|
|
9345
|
+
const mine = all.find((r) => r.projectId === projectId && r.kind === "voyage" && r.authorId === me);
|
|
9346
|
+
if (!text) {
|
|
9347
|
+
if (mine) write({ reflections: all.filter((r) => r.id !== mine.id) });
|
|
9348
|
+
return;
|
|
9349
|
+
}
|
|
9350
|
+
if (mine) {
|
|
9351
|
+
if (mine.body !== text) write({ reflections: all.map((r) => r.id === mine.id ? {
|
|
9352
|
+
...r,
|
|
9353
|
+
body: text
|
|
9354
|
+
} : r) });
|
|
9355
|
+
return;
|
|
9356
|
+
}
|
|
9357
|
+
if (!get().projects.some((p) => p.id === projectId)) return;
|
|
9358
|
+
const row = {
|
|
9359
|
+
id: uid("rf"),
|
|
9360
|
+
projectId,
|
|
9361
|
+
kind: "voyage",
|
|
9362
|
+
authorId: me,
|
|
9363
|
+
body: text,
|
|
9364
|
+
createdAt: stamp()
|
|
9365
|
+
};
|
|
9366
|
+
write({ reflections: [...all, row] });
|
|
9339
9367
|
}
|
|
9340
9368
|
};
|
|
9341
9369
|
}
|
package/lib/cli.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
harbors,
|
|
21
21
|
myWork,
|
|
22
22
|
premortemText,
|
|
23
|
+
voyageText,
|
|
23
24
|
raidList,
|
|
24
25
|
programTree,
|
|
25
26
|
projectList,
|
|
@@ -93,6 +94,8 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
93
94
|
harbor premortem PROJECT ["WHAT WENT WRONG"] [--start]
|
|
94
95
|
a project's pre-mortem: start,
|
|
95
96
|
answer or read it
|
|
97
|
+
harbor voyage PROJECT ["YOUR LINE"] a Done project's voyage log, and
|
|
98
|
+
your line in it
|
|
96
99
|
|
|
97
100
|
Every command takes --json, and --harbor NAME when you're in more than one
|
|
98
101
|
harbor (or set one with harbor use). harbor help COMMAND shows just that one.
|
|
@@ -242,6 +245,11 @@ export function hint(message) {
|
|
|
242
245
|
"Say which with --harbor NAME, or set one with harbor use NAME.",
|
|
243
246
|
)
|
|
244
247
|
.replace(/Find it with search_tasks\.$/, "Find it with harbor tasks.")
|
|
248
|
+
.replace(
|
|
249
|
+
/^(.+) has no pre-mortem running\. Start one with start_premortem first\.$/s,
|
|
250
|
+
(_, name) =>
|
|
251
|
+
`${name} has no pre-mortem running. Start one with harbor premortem "${name}" --start.`,
|
|
252
|
+
)
|
|
245
253
|
.replace(
|
|
246
254
|
/^Nothing to change: pass a field to update\.$/,
|
|
247
255
|
"Nothing to change. Say what with --title, --assignee, --due, --priority, --status, --notes, --repeat or --waits-on.",
|
|
@@ -260,6 +268,36 @@ export function hint(message) {
|
|
|
260
268
|
);
|
|
261
269
|
}
|
|
262
270
|
|
|
271
|
+
/** get_premortem's answer with its note naming the command, not the agent tool, since --json shows it. */
|
|
272
|
+
export function premortemForTerminal(r) {
|
|
273
|
+
if (r.running || !r.note) return r;
|
|
274
|
+
return {
|
|
275
|
+
...r,
|
|
276
|
+
note: `No pre-mortem on ${r.project} yet. Start one with harbor premortem "${r.project}" --start.`,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Commands whose forms do different things: one line each, saying which read and which write. */
|
|
281
|
+
const DETAIL = {
|
|
282
|
+
premortem: [
|
|
283
|
+
" harbor premortem PROJECT reads it: the question, the time left,",
|
|
284
|
+
" and the answers you may see",
|
|
285
|
+
" harbor premortem PROJECT --start writes: starts one (five minutes of",
|
|
286
|
+
" answering alone, then they show)",
|
|
287
|
+
' harbor premortem PROJECT "ANSWER" writes: adds your answer',
|
|
288
|
+
' harbor premortem PROJECT "ANSWER" --start',
|
|
289
|
+
" writes: starts one and adds your answer",
|
|
290
|
+
"",
|
|
291
|
+
" PROJECT is a name or an id. When two projects share a name, give the id.",
|
|
292
|
+
],
|
|
293
|
+
voyage: [
|
|
294
|
+
" harbor voyage PROJECT reads a Done project's voyage log",
|
|
295
|
+
' harbor voyage PROJECT "YOUR LINE" writes: adds or changes your line in it',
|
|
296
|
+
"",
|
|
297
|
+
" PROJECT is a name or an id. When two projects share a name, give the id.",
|
|
298
|
+
],
|
|
299
|
+
};
|
|
300
|
+
|
|
263
301
|
/** The lines of the help about one command, or null when it has none. */
|
|
264
302
|
export function commandHelp(command) {
|
|
265
303
|
const lines = HELP.split("\n");
|
|
@@ -275,6 +313,7 @@ export function commandHelp(command) {
|
|
|
275
313
|
while (lines[i + 1]?.startsWith(" ") && /^\s+\[|^\s+--/.test(lines[i + 1]))
|
|
276
314
|
picked.push(lines[++i]);
|
|
277
315
|
}
|
|
316
|
+
if (DETAIL[command]) picked.splice(0, picked.length, ...DETAIL[command]);
|
|
278
317
|
if (picked.length === 0) return null;
|
|
279
318
|
const common = ["signup", "login", "logout", "use", "theme", "help", "app"].includes(command)
|
|
280
319
|
? []
|
|
@@ -588,11 +627,13 @@ export async function run(argv, io = {}) {
|
|
|
588
627
|
width: process.stdout.isTTY ? process.stdout.columns : undefined,
|
|
589
628
|
};
|
|
590
629
|
|
|
591
|
-
const call = async (name, args, show) => {
|
|
592
|
-
const result =
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
630
|
+
const call = async (name, args, show, forTerminal = (r) => r) => {
|
|
631
|
+
const result = forTerminal(
|
|
632
|
+
await client.call(name, args).catch((err) => {
|
|
633
|
+
if (err instanceof HarborError) throw new HarborError(hint(err.message));
|
|
634
|
+
throw err;
|
|
635
|
+
}),
|
|
636
|
+
);
|
|
596
637
|
// The prompt's counts are behind now; its next draw fetches new ones.
|
|
597
638
|
if (name === "create_task" || name === "update_task") await stalePromptCache(env);
|
|
598
639
|
out(asJson ? jsonText(result) : plain(show(result)));
|
|
@@ -1013,7 +1054,21 @@ export async function run(argv, io = {}) {
|
|
|
1013
1054
|
: `Added your answer (${r.answered.yourAnswers} so far).`,
|
|
1014
1055
|
);
|
|
1015
1056
|
}
|
|
1016
|
-
return call("get_premortem", { harbor, project }, premortemText);
|
|
1057
|
+
return call("get_premortem", { harbor, project }, premortemText, premortemForTerminal);
|
|
1058
|
+
}
|
|
1059
|
+
case "voyage": {
|
|
1060
|
+
const { positionals, harbor } = parse(spec());
|
|
1061
|
+
const [project, ...words] = positionals;
|
|
1062
|
+
if (!project) throw new HarborError("Give a project, and your line if you're adding one.");
|
|
1063
|
+
const line = words.join(" ").trim();
|
|
1064
|
+
if (line) {
|
|
1065
|
+
return call(
|
|
1066
|
+
"add_voyage_line",
|
|
1067
|
+
{ harbor, project, line },
|
|
1068
|
+
(r) => `Your line is in ${r.added.project}'s voyage log.`,
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
return call("get_voyage_log", { harbor, project }, voyageText);
|
|
1017
1072
|
}
|
|
1018
1073
|
case "risk":
|
|
1019
1074
|
case "issue":
|
package/lib/format.mjs
CHANGED
|
@@ -211,3 +211,15 @@ export function premortemText(r) {
|
|
|
211
211
|
lines.push(` Nothing yet. Add one: harbor premortem "${r.project}" "WHAT WENT WRONG"`);
|
|
212
212
|
return [...head, "", ...lines].join("\n");
|
|
213
213
|
}
|
|
214
|
+
|
|
215
|
+
/** A project's voyage log, as get_voyage_log answers it. */
|
|
216
|
+
export function voyageText(r) {
|
|
217
|
+
const head = [`Voyage log · ${r.project}${r.done ? ` · Done ${r.finished}` : " · not Done yet"}`];
|
|
218
|
+
if (r.note) head.push(r.note);
|
|
219
|
+
head.push((r.stats ?? []).join(" · "));
|
|
220
|
+
const story = (r.story ?? []).map((s) => ` - ${s}`);
|
|
221
|
+
const crew = (r.crew ?? []).map((c) => ` - ${c.who ?? "Someone"}: ${c.line}`);
|
|
222
|
+
return [...head, "", ...story, ...(crew.length ? ["", "From the crew:", ...crew] : [])].join(
|
|
223
|
+
"\n",
|
|
224
|
+
);
|
|
225
|
+
}
|