@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 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 = await client.call(name, args).catch((err) => {
593
- if (err instanceof HarborError) throw new HarborError(hint(err.message));
594
- throw err;
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.16.0",
3
+ "version": "0.18.1",
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",