@harborppm/cli 0.28.0 → 0.30.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.
Files changed (3) hide show
  1. package/README.md +5 -2
  2. package/lib/cli.mjs +118 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -57,7 +57,7 @@ Your harbor is the same one at [harborppm.com](https://harborppm.com/today?utm_s
57
57
  - **GitHub**: a pull request that names a task's key (RR-42) shows on the task and moves it along.
58
58
  - **Claude and other AI apps**, connected over MCP from [harborppm.com/ai](https://harborppm.com/ai?utm_source=npm&utm_medium=readme&utm_campaign=cli).
59
59
 
60
- `harbor open RR-42` opens a task on the website (in the right harbor, even when the browser had another one open), `harbor open Ridge` a project, and `harbor open` on its own opens Today. `harbor invite sam@example.com` sends Sam the same invite letter the People page does (`--viewer` to let them look but not change anything); only a harbor's owner can invite. `harbor calendar` prints your private calendar link, to add to Google, Apple or Outlook calendar: your due dates, milestones and sign-offs beside your meetings, kept up to date (`--program NAME` for everything dated in one program, `--new` to swap the link for a fresh one, `--off` to turn it off).
60
+ `harbor open RR-42` opens a task on the website (in the right harbor, even when the browser had another one open), `harbor open Ridge` a project, and `harbor open` on its own opens Today. `harbor invite sam@example.com` sends Sam the same invite letter the People page does (`--viewer` to let them look but not change anything); only a harbor's owner can invite. `harbor calendar` prints your private calendar link, to add to Google, Apple or Outlook calendar: your due dates, milestones and sign-offs beside your meetings, kept up to date (`--program NAME` for everything dated in one program, `--new` to swap the link for a fresh one, `--off` to turn it off). `harbor roadmap "Next steps"` publishes a project as a public roadmap page (Now, Next and Shipped, task titles only) and prints its link; `--dates` adds the day each thing shipped and `--off` takes it down. Only a harbor's owner can publish.
61
61
 
62
62
  The website has the commands too: turn on **Command line at the bottom** in your menu, press `/`, and type `mine`, `add "Call the mill" #ridge @sam --due fri` or `done RR-12`.
63
63
 
@@ -103,6 +103,7 @@ harbor show TASK one task in full, with its web link
103
103
  harbor open [TASK|PROJECT] open it on the website (or Today)
104
104
  harbor invite EMAIL [--viewer] invite someone to your harbor by email
105
105
  harbor calendar [--program P] [--new|--off] your private calendar link (.ics)
106
+ harbor roadmap PROJECT [--dates] [--off] publish a project's public roadmap
106
107
 
107
108
  harbor program "NAME" [--portfolio P] [--lead me|NAME] [--starts DATE]
108
109
  [--ends DATE]
@@ -123,7 +124,7 @@ harbor issue "TITLE" --project P|--program P [--impact 1-5] [--owner NAME]
123
124
  [--due DATE]
124
125
  harbor decision "TITLE" --project P|--program P [--owner NAME] [--due DATE]
125
126
  harbor risks [words] [--kind risk,issue,decision] [--project P] [--closed]
126
- harbor brief [--portfolio P] the weekly brief, ready to send
127
+ harbor brief [--portfolio P] [--say] the weekly brief, ready to send
127
128
  harbor premortem PROJECT ["WHAT WENT WRONG"] [--start]
128
129
  harbor voyage PROJECT ["YOUR LINE"]
129
130
  harbor limit PROJECT [COLUMN N|none]
@@ -133,6 +134,8 @@ harbor limit PROJECT [COLUMN N|none]
133
134
 
134
135
  `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.
135
136
 
137
+ `harbor brief --say` turns the brief into the BBC's Shipping Forecast: each program an area, wind for late work, visibility for health, gale warnings for anything off track. It prints the forecast and speaks it with your computer's own voice (`say` on a Mac, Windows' speech, or `espeak-ng` on Linux).
138
+
136
139
  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.
137
140
 
138
141
  When you belong to more than one harbor, say which: `--harbor NAME` on the command, or `harbor use NAME` once (it's saved with your sign-in; `harbor use none` forgets it). With neither, the commands ask rather than guess, so nothing lands in the wrong harbor. The full-screen app shares the same choice: it opens the harbor `harbor use` set, asks which one the first time if none is set, and a harbor you switch to in the app becomes the one the commands use.
package/lib/cli.mjs CHANGED
@@ -79,6 +79,8 @@ export const HELP = `harbor: Harbor from the terminal
79
79
  harbor prompt --init SHELL how to add it: zsh, bash, fish
80
80
  or starship
81
81
  harbor calendar [--program P] [--new|--off] your private calendar link (.ics)
82
+ harbor roadmap PROJECT [--dates|--no-dates] [--off]
83
+ publish a project's public roadmap
82
84
 
83
85
  harbor program "NAME" [--portfolio P] [--lead me|NAME] [--starts DATE]
84
86
  [--ends DATE]
@@ -104,7 +106,9 @@ export const HELP = `harbor: Harbor from the terminal
104
106
  harbor risks [words] [--kind risk,issue,decision] [--project P] [--closed]
105
107
  the register, worst first
106
108
  (--closed adds closed ones)
107
- harbor brief [--portfolio P] the weekly brief, ready to send
109
+ harbor brief [--portfolio P] [--say] the weekly brief, ready to send;
110
+ --say reads it aloud as a
111
+ shipping forecast
108
112
  harbor premortem PROJECT ["WHAT WENT WRONG"] [--start]
109
113
  a project's pre-mortem: start,
110
114
  answer or read it
@@ -172,6 +176,56 @@ function openBrowser(url) {
172
176
  }
173
177
  }
174
178
 
179
+ /**
180
+ * How to read text aloud here, as programs to try in turn, each with its
181
+ * arguments (never through a shell). Text goes on stdin where it can, so
182
+ * nothing in it is read as an option.
183
+ */
184
+ export function speechCommands(platform = process.platform) {
185
+ if (platform === "darwin") return [["say", ["-f", "-"]]];
186
+ if (platform === "win32")
187
+ return [
188
+ [
189
+ "powershell",
190
+ [
191
+ "-NoProfile",
192
+ "-Command",
193
+ "Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).Speak([Console]::In.ReadToEnd())",
194
+ ],
195
+ ],
196
+ ];
197
+ return [
198
+ ["espeak-ng", ["--stdin", "-s", "155"]],
199
+ ["espeak", ["--stdin", "-s", "155"]],
200
+ ["spd-say", ["-w", "-e"]],
201
+ ];
202
+ }
203
+
204
+ /** Read `text` aloud with the first speech program this machine has; false when none ran. */
205
+ async function speak(text) {
206
+ for (const [program, args] of speechCommands()) {
207
+ const ok = await new Promise((resolve) => {
208
+ try {
209
+ const child = spawn(program, args, { stdio: ["pipe", "ignore", "ignore"] });
210
+ child.on("error", () => resolve(false));
211
+ child.on("close", (code) => resolve(code === 0));
212
+ child.stdin.on("error", () => {});
213
+ child.stdin.end(text);
214
+ } catch {
215
+ resolve(false);
216
+ }
217
+ });
218
+ if (ok) return true;
219
+ }
220
+ return false;
221
+ }
222
+
223
+ /** This machine's wall-clock time as YYYY-MM-DDTHH:MM, which Harbor reads as the caller's hour. */
224
+ export function localMinute(now = new Date()) {
225
+ const pad = (n) => String(n).padStart(2, "0");
226
+ return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;
227
+ }
228
+
175
229
  const one = (positionals, what) => {
176
230
  if (positionals.length !== 1) throw new HarborError(`Give one ${what}.`);
177
231
  return positionals[0];
@@ -1011,6 +1065,46 @@ export async function run(argv, io = {}) {
1011
1065
  );
1012
1066
  return 0;
1013
1067
  }
1068
+ case "roadmap": {
1069
+ const { positionals, values, harbor } = parse(
1070
+ spec(["dates", "no-dates", "site", "no-site", "off"]),
1071
+ );
1072
+ const project = text(positionals, "which project");
1073
+ if (values.dates && values["no-dates"])
1074
+ throw new HarborError("Say --dates or --no-dates, not both.");
1075
+ if (values.site && values["no-site"])
1076
+ throw new HarborError("Say --site or --no-site, not both.");
1077
+ if (!hasFullAccess(creds)) {
1078
+ throw new HarborError("Publishing needs full access. Run harbor login, then try again.");
1079
+ }
1080
+ const r = await client.app({
1081
+ op: "roadmap",
1082
+ ...(harbor ? { harbor } : {}),
1083
+ project,
1084
+ ...(values.off ? { off: true } : {}),
1085
+ ...(values.dates || values["no-dates"] ? { showDates: Boolean(values.dates) } : {}),
1086
+ ...(values.site || values["no-site"] ? { onSite: Boolean(values.site) } : {}),
1087
+ });
1088
+ if (asJson) {
1089
+ out(jsonText(r));
1090
+ return 0;
1091
+ }
1092
+ if (!r.url) {
1093
+ out(
1094
+ plain(
1095
+ `${r.project} isn't published any more. Its link says the roadmap isn't published.`,
1096
+ ),
1097
+ );
1098
+ return 0;
1099
+ }
1100
+ const shows = r.showDates ? "titles and the day each shipped" : "titles only";
1101
+ out(
1102
+ plain(
1103
+ `${r.project}'s public roadmap (Now, Next and Shipped; ${shows}, no people):\n\n ${r.url}${r.siteUrl ? `\n\nIt's also Harbor's own roadmap at ${r.siteUrl}.` : ""}\n\nharbor roadmap "${r.project}" --off takes it down.`,
1104
+ ),
1105
+ );
1106
+ return 0;
1107
+ }
1014
1108
  case "add": {
1015
1109
  const { positionals, values, harbor } = parse(
1016
1110
  spec(["over-limit"], ["project", "assignee", "due", "priority", "status"]),
@@ -1123,6 +1217,29 @@ export async function run(argv, io = {}) {
1123
1217
  () => "Checked in for today.",
1124
1218
  );
1125
1219
  }
1220
+ case "brief": {
1221
+ const { positionals, values, harbor } = parse(spec(["say"], ["portfolio"]));
1222
+ none(positionals);
1223
+ if (!values.say) {
1224
+ return call("weekly_brief", { harbor, portfolio: values.portfolio }, (r) => r.text);
1225
+ }
1226
+ const result = await client
1227
+ .call("shipping_forecast", { harbor, portfolio: values.portfolio, at: localMinute() })
1228
+ .catch((err) => {
1229
+ if (err instanceof HarborError) throw new HarborError(hint(err.message));
1230
+ throw err;
1231
+ });
1232
+ const script = plain(result.script ?? "");
1233
+ out(asJson ? jsonText(result) : script);
1234
+ if (!(await (io.speak ?? speak)(script))) {
1235
+ throw new HarborError(
1236
+ process.platform === "linux"
1237
+ ? "Nothing here can read aloud. Install espeak-ng (or speech-dispatcher) and try again."
1238
+ : "Nothing here can read aloud.",
1239
+ );
1240
+ }
1241
+ return 0;
1242
+ }
1126
1243
  case "risks": {
1127
1244
  const { positionals, values, harbor } = parse(spec(["closed"], ["kind", "project"]));
1128
1245
  const kind = values.kind
@@ -1143,11 +1260,6 @@ export async function run(argv, io = {}) {
1143
1260
  (r) => raidList(r, positionals.join(" ")),
1144
1261
  );
1145
1262
  }
1146
- case "brief": {
1147
- const { positionals, values, harbor } = parse(spec([], ["portfolio"]));
1148
- none(positionals);
1149
- return call("weekly_brief", { harbor, portfolio: values.portfolio }, (r) => r.text);
1150
- }
1151
1263
  case "premortem": {
1152
1264
  const { positionals, values, harbor } = parse(spec(["start"]));
1153
1265
  const [project, ...words] = positionals;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.28.0",
3
+ "version": "0.30.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",