@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.
- package/README.md +3 -1
- package/lib/cli.mjs +76 -6
- 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]
|
|
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]
|
|
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;
|