@harborppm/cli 0.88.0 → 0.90.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/bin/harbor.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { VERSION, run } from "../lib/cli.mjs";
3
+ import { exitWhenWritten } from "../lib/exit.mjs";
3
4
  import { HarborError } from "../lib/mcp.mjs";
4
5
  import { jsonText, plain } from "../lib/plain.mjs";
5
6
  import { updateNotice } from "../lib/update.mjs";
@@ -19,7 +20,7 @@ const tell = async () => {
19
20
  run(argv).then(
20
21
  async (code) => {
21
22
  await tell();
22
- process.exit(code);
23
+ await exitWhenWritten(code);
23
24
  },
24
25
  async (err) => {
25
26
  const known =
@@ -33,6 +34,6 @@ run(argv).then(
33
34
  if (argv.includes("--json")) process.stdout.write(`${jsonText({ error: message })}\n`);
34
35
  else process.stderr.write(`${plain(message)}\n`);
35
36
  await tell();
36
- process.exit(1);
37
+ await exitWhenWritten(1);
37
38
  },
38
39
  );
package/dist/app.mjs CHANGED
@@ -4944,7 +4944,7 @@ function normalizeBoardConfig(raw) {
4944
4944
  const todo = columns.find((c) => c.status === "todo") ?? columns[0];
4945
4945
  todo.visible = true;
4946
4946
  }
4947
- const swimlane = raw.swimlane === "assignee" || raw.swimlane === "priority" ? raw.swimlane : "none";
4947
+ const swimlane = raw.swimlane === "assignee" || raw.swimlane === "priority" ? raw.swimlane : typeof raw.swimlane === "string" && /^field:[^\s]{1,100}$/.test(raw.swimlane) ? raw.swimlane : "none";
4948
4948
  const cardFields = {
4949
4949
  dueDate: raw.cardFields?.dueDate !== false,
4950
4950
  assignee: raw.cardFields?.assignee !== false,
@@ -19723,6 +19723,18 @@ function undatedText(pole) {
19723
19723
  return n === 1 ? "1 open task has no due date, so it can't hold the date." : `${n} open tasks have no due date, so they can't hold the date.`;
19724
19724
  }
19725
19725
 
19726
+ // src/lib/raid-register.ts
19727
+ init_raid();
19728
+ /**
19729
+ * What agents and `harbor risks` list unless asked for everything: anything
19730
+ * still needing attention, plus decisions that stand. A decision is decided
19731
+ * once and then holds, so leaving it out the moment it's settled read as the
19732
+ * decision vanishing. Only one replaced by a newer decision drops out.
19733
+ */
19734
+ function onLiveRegister(item) {
19735
+ return isActive(item) || item.kind === "decision" && item.status === "decided";
19736
+ }
19737
+
19726
19738
  // src/lib/csv.ts
19727
19739
  /**
19728
19740
  * Text a spreadsheet would run as a formula: a title from an intake form
@@ -21756,7 +21768,7 @@ const HARBOR_TOOLS = [
21756
21768
  tool({
21757
21769
  name: "list_risks",
21758
21770
  title: "Risks, issues and decisions",
21759
- description: "The RAID register, highest score first: open risks, issues and pending decisions unless asked for all." + DATA_NOTE,
21771
+ description: "The RAID register, highest score first: open risks and issues, and decisions still to make or standing, unless asked for all (closed, resolved and replaced ones too). Each item says when it was logged and last changed." + DATA_NOTE,
21760
21772
  input: object({
21761
21773
  harbor: harborArg,
21762
21774
  kind: array(_enum([
@@ -21773,7 +21785,7 @@ const HARBOR_TOOLS = [
21773
21785
  const { data } = ctx;
21774
21786
  const people = byId(data.people);
21775
21787
  const project = input.project ? findOne(data.projects, input.project, "project") : null;
21776
- const list = data.raidItems.filter((r) => (input.includeClosed || isActive(r)) && (!input.kind?.length || input.kind.includes(r.kind)) && (!project || r.projectId === project.id)).sort((a, b) => raidScore(b) - raidScore(a));
21788
+ const list = data.raidItems.filter((r) => (input.includeClosed || onLiveRegister(r)) && (!input.kind?.length || input.kind.includes(r.kind)) && (!project || r.projectId === project.id)).sort((a, b) => raidScore(b) - raidScore(a));
21777
21789
  return {
21778
21790
  total: list.length,
21779
21791
  items: list.slice(input.offset, input.offset + input.limit).map((r) => ({
@@ -21789,7 +21801,9 @@ const HARBOR_TOOLS = [
21789
21801
  due: dayOf(r.dueDate),
21790
21802
  ...raidScope(data, r),
21791
21803
  ...r.mitigation ? { mitigation: r.mitigation } : {},
21792
- ...r.resolution ? { resolution: r.resolution } : {}
21804
+ ...r.resolution ? { resolution: r.resolution } : {},
21805
+ createdAt: r.createdAt,
21806
+ updatedAt: r.updatedAt
21793
21807
  }))
21794
21808
  };
21795
21809
  }
@@ -30011,6 +30025,15 @@ function defaultAction(type) {
30011
30025
  };
30012
30026
  }
30013
30027
 
30028
+ // src/lib/board-lanes.ts
30029
+ /** Fields a board can be split into lanes by. */
30030
+ function isLaneField(def) {
30031
+ return def.type === "select" || def.type === "multi" || def.type === "person" || def.type === "checkbox" || def.type === "date";
30032
+ }
30033
+ function fieldLane(fieldId) {
30034
+ return `field:${fieldId}`;
30035
+ }
30036
+
30014
30037
  // src/tui/views/setup.tsx
30015
30038
  /**
30016
30039
  * A project's board setup, as the website's board settings have it: where it
@@ -30077,6 +30100,10 @@ function BoardPanel({ id, width, height, go, active, onClose }) {
30077
30100
  const people = useHarbor((s) => s.people);
30078
30101
  if (!project) return null;
30079
30102
  const config = boardConfigOf({ boardConfigs }, id);
30103
+ const lanes = [...LANES, ...(project.customFields ?? []).filter(isLaneField).map((f) => ({
30104
+ value: fieldLane(f.id),
30105
+ label: `By ${f.name}`
30106
+ }))];
30080
30107
  const set = (patch) => store.getState().setBoardConfig(id, {
30081
30108
  ...config,
30082
30109
  preset: "custom",
@@ -30159,15 +30186,15 @@ function BoardPanel({ id, width, height, go, active, onClose }) {
30159
30186
  ...config.columns.map(columnField),
30160
30187
  {
30161
30188
  label: "Lanes",
30162
- value: LANES.find((l) => l.value === config.swimlane)?.label ?? "None",
30189
+ value: lanes.find((l) => l.value === config.swimlane)?.label ?? "None",
30163
30190
  edit: {
30164
30191
  kind: "pick",
30165
30192
  title: "Lanes across the website's board",
30166
- choices: LANES.map((l) => ({
30193
+ choices: lanes.map((l) => ({
30167
30194
  label: l.label,
30168
30195
  value: l.value
30169
30196
  })),
30170
- initial: LANES.findIndex((l) => l.value === config.swimlane),
30197
+ initial: Math.max(0, lanes.findIndex((l) => l.value === config.swimlane)),
30171
30198
  onPick: (v) => set({ swimlane: v })
30172
30199
  }
30173
30200
  },
package/lib/exit.mjs ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Leaving once what was written has left. Into a pipe, stdout is written
3
+ * asynchronously, so `process.exit()` straight after a big write cut
4
+ * `harbor export harbor | jq` off at 64 KB with no error. A zero-length
5
+ * write's callback runs after every write before it, so waiting on one per
6
+ * stream is enough; the exit after it still stops anything left running.
7
+ */
8
+ const flushed = (stream) =>
9
+ new Promise((resolve) => {
10
+ if (!stream.writableLength) resolve();
11
+ else stream.write("", () => resolve());
12
+ });
13
+
14
+ export async function exitWhenWritten(code, streams = [process.stdout, process.stderr]) {
15
+ process.exitCode = code;
16
+ await Promise.all(streams.map(flushed));
17
+ process.exit(code);
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.88.0",
3
+ "version": "0.90.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",