@harborppm/cli 0.29.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 +3 -1
  2. package/lib/cli.mjs +76 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -124,7 +124,7 @@ harbor issue "TITLE" --project P|--program P [--impact 1-5] [--owner NAME]
124
124
  [--due DATE]
125
125
  harbor decision "TITLE" --project P|--program P [--owner NAME] [--due DATE]
126
126
  harbor risks [words] [--kind risk,issue,decision] [--project P] [--closed]
127
- harbor brief [--portfolio P] the weekly brief, ready to send
127
+ harbor brief [--portfolio P] [--say] the weekly brief, ready to send
128
128
  harbor premortem PROJECT ["WHAT WENT WRONG"] [--start]
129
129
  harbor voyage PROJECT ["YOUR LINE"]
130
130
  harbor limit PROJECT [COLUMN N|none]
@@ -134,6 +134,8 @@ harbor limit PROJECT [COLUMN N|none]
134
134
 
135
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.
136
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
+
137
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.
138
140
 
139
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
@@ -106,7 +106,9 @@ export const HELP = `harbor: Harbor from the terminal
106
106
  harbor risks [words] [--kind risk,issue,decision] [--project P] [--closed]
107
107
  the register, worst first
108
108
  (--closed adds closed ones)
109
- 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
110
112
  harbor premortem PROJECT ["WHAT WENT WRONG"] [--start]
111
113
  a project's pre-mortem: start,
112
114
  answer or read it
@@ -174,6 +176,56 @@ function openBrowser(url) {
174
176
  }
175
177
  }
176
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
+
177
229
  const one = (positionals, what) => {
178
230
  if (positionals.length !== 1) throw new HarborError(`Give one ${what}.`);
179
231
  return positionals[0];
@@ -1165,6 +1217,29 @@ export async function run(argv, io = {}) {
1165
1217
  () => "Checked in for today.",
1166
1218
  );
1167
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
+ }
1168
1243
  case "risks": {
1169
1244
  const { positionals, values, harbor } = parse(spec(["closed"], ["kind", "project"]));
1170
1245
  const kind = values.kind
@@ -1185,11 +1260,6 @@ export async function run(argv, io = {}) {
1185
1260
  (r) => raidList(r, positionals.join(" ")),
1186
1261
  );
1187
1262
  }
1188
- case "brief": {
1189
- const { positionals, values, harbor } = parse(spec([], ["portfolio"]));
1190
- none(positionals);
1191
- return call("weekly_brief", { harbor, portfolio: values.portfolio }, (r) => r.text);
1192
- }
1193
1263
  case "premortem": {
1194
1264
  const { positionals, values, harbor } = parse(spec(["start"]));
1195
1265
  const [project, ...words] = positionals;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.29.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",