@harborppm/cli 0.38.0 → 0.40.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 +6 -5
- package/dist/app.mjs +10 -8
- package/lib/cli.mjs +100 -31
- package/lib/format.mjs +28 -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). `harbor roadmap "Next steps"`
|
|
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"` says whether a project is a public roadmap page and prints its link; `--publish` publishes it (Now, Next and Shipped, task titles only, on a link that can't be guessed, which stays the same while it's live), `--publish --dates` adds the day each thing shipped (titles only is the default), and `--off` takes it down at once. Only a harbor's owner can publish. `harbor projects --json` gives each project's `public` and `url`.
|
|
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
|
|
|
@@ -115,6 +115,7 @@ Task titles, names and notes can come from an intake form, a GitHub pull request
|
|
|
115
115
|
|
|
116
116
|
```
|
|
117
117
|
harbor harbors the harbors you belong to
|
|
118
|
+
harbor whoami who you're signed in as, and where
|
|
118
119
|
harbor use HARBOR|none the harbor to work in when you're in several
|
|
119
120
|
harbor programs programs and their projects
|
|
120
121
|
harbor projects [words] [--program P] projects with health and open work
|
|
@@ -124,12 +125,12 @@ harbor hill [--program P] where each project is on its hill
|
|
|
124
125
|
harbor hill PROJECT --at 0-100|top|done move it: up to 50 is figuring it out,
|
|
125
126
|
past 50 is getting it done
|
|
126
127
|
harbor mine what's on you, late first
|
|
127
|
-
harbor tasks [words] [--project P] [--assignee me|none|NAME] [--late] [--status S,S]
|
|
128
|
+
harbor tasks [words] [--project P] [--assignee me|none|NAME] [--late] [--status S,S] [--all]
|
|
128
129
|
harbor show TASK one task in full, with its web link
|
|
129
130
|
harbor open [TASK|PROJECT] open it on the website (or Today)
|
|
130
131
|
harbor invite EMAIL [--viewer] invite someone to your harbor by email
|
|
131
132
|
harbor calendar [--program P] [--new|--off] your private calendar link (.ics)
|
|
132
|
-
harbor roadmap PROJECT [--
|
|
133
|
+
harbor roadmap PROJECT [--publish] [--off] a project's public roadmap: read, publish or take it down
|
|
133
134
|
|
|
134
135
|
harbor program "NAME" [--portfolio P] [--lead me|NAME] [--starts DATE]
|
|
135
136
|
[--ends DATE]
|
|
@@ -150,7 +151,7 @@ harbor issue "TITLE" --project P|--program P [--impact 1-5] [--owner NAME]
|
|
|
150
151
|
[--due DATE]
|
|
151
152
|
harbor decision "TITLE" --project P|--program P [--owner NAME] [--due DATE]
|
|
152
153
|
harbor risks [words] [--kind risk,issue,decision] [--project P] [--closed]
|
|
153
|
-
harbor brief [--portfolio P] [--say]
|
|
154
|
+
harbor brief [--portfolio P] [--all] [--say] the weekly brief (--all: every late and done task)
|
|
154
155
|
harbor report PROGRAM a program's status report, as Markdown
|
|
155
156
|
harbor export tasks PROJECT|risks|review [--all]
|
|
156
157
|
a spreadsheet (CSV) to stdout
|
|
@@ -177,7 +178,7 @@ Agents on the crew: put an AI app on People with Add an agent and assign it task
|
|
|
177
178
|
|
|
178
179
|
`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. `harbor note` leaves a note for later on a project: it stays sealed (the crew sees only that it's there and who left it) until its day, until one of the program's milestones is reached, or until the project turns Off track, and then it opens at the top of the project and in the brief.
|
|
179
180
|
|
|
180
|
-
`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).
|
|
181
|
+
`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, which you may need to install). With `--json` it prints the forecast and stays quiet.
|
|
181
182
|
|
|
182
183
|
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.
|
|
183
184
|
|
package/dist/app.mjs
CHANGED
|
@@ -5013,7 +5013,7 @@ function saidThisWeek(data, now, inScope) {
|
|
|
5013
5013
|
}] : [];
|
|
5014
5014
|
});
|
|
5015
5015
|
}
|
|
5016
|
-
/** How many late tasks the brief names before "and N more". */
|
|
5016
|
+
/** How many late (or finished) tasks the brief names before "and N more". */
|
|
5017
5017
|
const BRIEF_LATE_SHOWN = 8;
|
|
5018
5018
|
function byEndDate(a, b) {
|
|
5019
5019
|
return a.endDate.localeCompare(b.endDate);
|
|
@@ -5065,6 +5065,7 @@ function lateOwnersOf(data, tasks, now) {
|
|
|
5065
5065
|
}
|
|
5066
5066
|
function composeBriefBody(opts) {
|
|
5067
5067
|
const date = format(opts.date, "MMMM d, yyyy");
|
|
5068
|
+
const shown = opts.all ? Infinity : 8;
|
|
5068
5069
|
const watch = opts.watch ?? [];
|
|
5069
5070
|
const milestones = opts.milestones ?? [];
|
|
5070
5071
|
const lateMilestones = milestones.filter((m) => m.daysLate > 0);
|
|
@@ -5081,8 +5082,8 @@ function composeBriefBody(opts) {
|
|
|
5081
5082
|
const done = () => {
|
|
5082
5083
|
if (!opts.shipped?.length) return;
|
|
5083
5084
|
lines.push("Done this week");
|
|
5084
|
-
for (const t of opts.shipped.slice(0,
|
|
5085
|
-
if (opts.shipped.length >
|
|
5085
|
+
for (const t of opts.shipped.slice(0, shown)) lines.push(`• ${t.title}${t.project ? ` (${t.project})` : ""}`);
|
|
5086
|
+
if (opts.shipped.length > shown) lines.push(`• and ${opts.shipped.length - shown} more`);
|
|
5086
5087
|
lines.push("");
|
|
5087
5088
|
};
|
|
5088
5089
|
const said = () => {
|
|
@@ -5128,12 +5129,12 @@ function composeBriefBody(opts) {
|
|
|
5128
5129
|
}
|
|
5129
5130
|
if (opts.late.length) {
|
|
5130
5131
|
lines.push("Late");
|
|
5131
|
-
for (const row of opts.late.slice(0,
|
|
5132
|
+
for (const row of opts.late.slice(0, shown)) {
|
|
5132
5133
|
const when = `${row.days} day${row.days === 1 ? "" : "s"} late`;
|
|
5133
5134
|
const held = row.holdsUp?.length ? ` (holds up ${nameList(row.holdsUp.map((t) => `“${t}”`))})` : "";
|
|
5134
5135
|
lines.push(`• ${row.title} — ${row.owner ? `${row.owner}, ` : ""}${when}${held}`);
|
|
5135
5136
|
}
|
|
5136
|
-
if (opts.late.length >
|
|
5137
|
+
if (opts.late.length > shown) lines.push(`• and ${opts.late.length - shown} more`);
|
|
5137
5138
|
lines.push("");
|
|
5138
5139
|
}
|
|
5139
5140
|
done();
|
|
@@ -5334,9 +5335,10 @@ function briefOf(data, opts = {}) {
|
|
|
5334
5335
|
quiet: grouped.offTrack.length === 0 && grouped.atRisk.length === 0 && lateOwners.length === 0 && watch.length === 0 && milestonesLate.length === 0
|
|
5335
5336
|
};
|
|
5336
5337
|
}
|
|
5337
|
-
function composeBriefBodyFrom(brief, data, workspace) {
|
|
5338
|
+
function composeBriefBodyFrom(brief, data, workspace, all = false) {
|
|
5338
5339
|
return composeBriefBody({
|
|
5339
5340
|
workspace,
|
|
5341
|
+
all,
|
|
5340
5342
|
date: brief.asOf,
|
|
5341
5343
|
offTrack: brief.offTrackPrograms,
|
|
5342
5344
|
atRisk: brief.atRiskPrograms,
|
|
@@ -5375,8 +5377,8 @@ function composeBriefBodyFrom(brief, data, workspace) {
|
|
|
5375
5377
|
}))
|
|
5376
5378
|
});
|
|
5377
5379
|
}
|
|
5378
|
-
function renderBriefText(brief, data, workspace = "Harbor") {
|
|
5379
|
-
return composeBriefBodyFrom(brief, data, workspace);
|
|
5380
|
+
function renderBriefText(brief, data, workspace = "Harbor", all = false) {
|
|
5381
|
+
return composeBriefBodyFrom(brief, data, workspace, all);
|
|
5380
5382
|
}
|
|
5381
5383
|
|
|
5382
5384
|
//#endregion
|
package/lib/cli.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
forecastOne,
|
|
24
24
|
STATUS,
|
|
25
25
|
harbors,
|
|
26
|
+
whoami,
|
|
26
27
|
limitsText,
|
|
27
28
|
hillData,
|
|
28
29
|
hillList,
|
|
@@ -62,6 +63,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
62
63
|
harbor login [--server URL] [--read-only] sign in (opens your browser once)
|
|
63
64
|
harbor logout forget the sign-in on this machine
|
|
64
65
|
harbor harbors the harbors you belong to
|
|
66
|
+
harbor whoami who you're signed in as, and where
|
|
65
67
|
harbor use HARBOR|none the harbor to work in
|
|
66
68
|
harbor theme [auto|light|dark] the app's colours
|
|
67
69
|
harbor --version which version this is
|
|
@@ -76,7 +78,9 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
76
78
|
out, past 50 is getting it done
|
|
77
79
|
harbor mine what's on you, late first
|
|
78
80
|
harbor tasks [words] [--project P] [--assignee me|none|NAME]
|
|
79
|
-
[--late] [--status S,S]
|
|
81
|
+
[--late] [--status S,S] [--all]
|
|
82
|
+
the first 20 (--all: every one);
|
|
83
|
+
done ones only with --status done
|
|
80
84
|
harbor show TASK one task (TASK: a key like RR-42)
|
|
81
85
|
harbor open [TASK|PROJECT] open it on the website (or Today)
|
|
82
86
|
harbor invite EMAIL [--viewer] invite someone to your harbor
|
|
@@ -85,8 +89,10 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
85
89
|
harbor prompt --init SHELL how to add it: zsh, bash, fish
|
|
86
90
|
or starship
|
|
87
91
|
harbor calendar [--program P] [--new|--off] your private calendar link (.ics)
|
|
88
|
-
harbor roadmap PROJECT [--dates|--no-dates] [--
|
|
89
|
-
|
|
92
|
+
harbor roadmap PROJECT [--publish [--dates|--no-dates] [--site|--no-site]]
|
|
93
|
+
[--off]
|
|
94
|
+
a project's public roadmap: read,
|
|
95
|
+
publish or take it down
|
|
90
96
|
|
|
91
97
|
harbor init [PROJECT] [--hook] keep a project's tasks as files in
|
|
92
98
|
this repository (.harbor/), one
|
|
@@ -120,9 +126,12 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
120
126
|
harbor risks [words] [--kind risk,issue,decision] [--project P] [--closed]
|
|
121
127
|
the register, worst first
|
|
122
128
|
(--closed adds closed ones)
|
|
123
|
-
harbor brief [--portfolio P] [--say]
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
harbor brief [--portfolio P] [--all] [--say]
|
|
130
|
+
the weekly brief, ready to send
|
|
131
|
+
(--all lists every late and done
|
|
132
|
+
task); --say reads it aloud as a
|
|
133
|
+
shipping forecast (on Linux it
|
|
134
|
+
needs espeak-ng)
|
|
126
135
|
harbor report PROGRAM a program's status report
|
|
127
136
|
harbor export tasks PROJECT|risks|review [--all]
|
|
128
137
|
a spreadsheet (CSV) to stdout
|
|
@@ -450,6 +459,29 @@ const DETAIL = {
|
|
|
450
459
|
" PROJECT is a name, a key like RR, or an id. When two projects share a",
|
|
451
460
|
" name, give the key.",
|
|
452
461
|
],
|
|
462
|
+
roadmap: [
|
|
463
|
+
" harbor roadmap PROJECT reads whether it's public, and its link",
|
|
464
|
+
" harbor roadmap PROJECT --publish writes: publishes it, or keeps it live",
|
|
465
|
+
" with any change below; the link stays",
|
|
466
|
+
" the same while it's live",
|
|
467
|
+
" harbor roadmap PROJECT --publish --dates",
|
|
468
|
+
" writes: also shows the day each thing",
|
|
469
|
+
" shipped (--no-dates: titles only, the",
|
|
470
|
+
" default)",
|
|
471
|
+
" harbor roadmap PROJECT --publish --site",
|
|
472
|
+
" writes: also shows it at",
|
|
473
|
+
" harborppm.com/roadmap, in place of any",
|
|
474
|
+
" other (Harbor's own team only;",
|
|
475
|
+
" --no-site, the default, takes it off)",
|
|
476
|
+
" harbor roadmap PROJECT --off writes: takes it down at once; the link",
|
|
477
|
+
" then says it isn't published, and",
|
|
478
|
+
" publishing again makes a new link",
|
|
479
|
+
"",
|
|
480
|
+
" A visitor sees Now, Next and Shipped: task titles only, with no people,",
|
|
481
|
+
" keys, notes, comments or due dates. Anyone with the link can open it; the",
|
|
482
|
+
" link is long and random, so it can't be guessed. Only the harbor's owner",
|
|
483
|
+
" can publish.",
|
|
484
|
+
],
|
|
453
485
|
forecast: [
|
|
454
486
|
" harbor forecast [--program P] reads when each open project will",
|
|
455
487
|
" likely finish",
|
|
@@ -1053,6 +1085,10 @@ export async function run(argv, io = {}) {
|
|
|
1053
1085
|
: harbors(r),
|
|
1054
1086
|
);
|
|
1055
1087
|
}
|
|
1088
|
+
case "whoami": {
|
|
1089
|
+
none(parse(spec()).positionals);
|
|
1090
|
+
return call("list_harbors", {}, (r) => whoami(r, creds));
|
|
1091
|
+
}
|
|
1056
1092
|
case "use": {
|
|
1057
1093
|
const { positionals } = parse({ flags: ["json"], options: [] });
|
|
1058
1094
|
const wanted = positionals.join(" ").trim();
|
|
@@ -1203,25 +1239,40 @@ export async function run(argv, io = {}) {
|
|
|
1203
1239
|
}
|
|
1204
1240
|
case "tasks": {
|
|
1205
1241
|
const { positionals, values, harbor } = parse(
|
|
1206
|
-
spec(["late"], ["project", "assignee", "status"]),
|
|
1207
|
-
);
|
|
1208
|
-
return call(
|
|
1209
|
-
"search_tasks",
|
|
1210
|
-
{
|
|
1211
|
-
harbor,
|
|
1212
|
-
query: positionals.join(" ") || undefined,
|
|
1213
|
-
project: values.project,
|
|
1214
|
-
assignee: values.assignee,
|
|
1215
|
-
status: values.status
|
|
1216
|
-
? values.status
|
|
1217
|
-
.split(",")
|
|
1218
|
-
.map((x) => x.trim())
|
|
1219
|
-
.filter(Boolean)
|
|
1220
|
-
: undefined,
|
|
1221
|
-
overdue: values.late || undefined,
|
|
1222
|
-
},
|
|
1223
|
-
(r) => taskList(r, opts),
|
|
1242
|
+
spec(["late", "all"], ["project", "assignee", "status"]),
|
|
1224
1243
|
);
|
|
1244
|
+
const args = {
|
|
1245
|
+
harbor,
|
|
1246
|
+
query: positionals.join(" ") || undefined,
|
|
1247
|
+
project: values.project,
|
|
1248
|
+
assignee: values.assignee,
|
|
1249
|
+
status: values.status
|
|
1250
|
+
? values.status
|
|
1251
|
+
.split(",")
|
|
1252
|
+
.map((x) => x.trim())
|
|
1253
|
+
.filter(Boolean)
|
|
1254
|
+
: undefined,
|
|
1255
|
+
overdue: values.late || undefined,
|
|
1256
|
+
};
|
|
1257
|
+
if (!values.all) return call("search_tasks", args, (r) => taskList(r, opts));
|
|
1258
|
+
// --all pages through every match, PAGE at a time (search_tasks' most).
|
|
1259
|
+
const PAGE = 50;
|
|
1260
|
+
const tasks = [];
|
|
1261
|
+
let first;
|
|
1262
|
+
do {
|
|
1263
|
+
const page = await client
|
|
1264
|
+
.call("search_tasks", { ...args, limit: PAGE, offset: tasks.length })
|
|
1265
|
+
.catch((err) => {
|
|
1266
|
+
if (err instanceof HarborError) throw new HarborError(hint(err.message));
|
|
1267
|
+
throw err;
|
|
1268
|
+
});
|
|
1269
|
+
first ??= page;
|
|
1270
|
+
tasks.push(...(page.tasks ?? []));
|
|
1271
|
+
if (!page.tasks?.length) break;
|
|
1272
|
+
} while (tasks.length < first.total);
|
|
1273
|
+
const result = { ...first, total: tasks.length, tasks };
|
|
1274
|
+
out(asJson ? jsonText(result) : plain(taskList(result, opts)));
|
|
1275
|
+
return 0;
|
|
1225
1276
|
}
|
|
1226
1277
|
case "show": {
|
|
1227
1278
|
const { positionals, harbor } = parse(spec());
|
|
@@ -1349,20 +1400,28 @@ export async function run(argv, io = {}) {
|
|
|
1349
1400
|
}
|
|
1350
1401
|
case "roadmap": {
|
|
1351
1402
|
const { positionals, values, harbor } = parse(
|
|
1352
|
-
spec(["dates", "no-dates", "site", "no-site", "off"]),
|
|
1403
|
+
spec(["publish", "dates", "no-dates", "site", "no-site", "off"]),
|
|
1353
1404
|
);
|
|
1354
1405
|
const project = text(positionals, "which project");
|
|
1355
1406
|
if (values.dates && values["no-dates"])
|
|
1356
1407
|
throw new HarborError("Say --dates or --no-dates, not both.");
|
|
1357
1408
|
if (values.site && values["no-site"])
|
|
1358
1409
|
throw new HarborError("Say --site or --no-site, not both.");
|
|
1410
|
+
if (values.off && values.publish) throw new HarborError("Say --publish or --off, not both.");
|
|
1411
|
+
const changes = ["dates", "no-dates", "site", "no-site"].filter((f) => values[f]);
|
|
1412
|
+
if (changes.length && !values.publish) {
|
|
1413
|
+
throw new HarborError(
|
|
1414
|
+
`Nothing changed. Add --publish to change what the page shows: harbor roadmap "${project}" --publish ${changes.map((f) => `--${f}`).join(" ")}`,
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1359
1417
|
if (!hasFullAccess(creds)) {
|
|
1360
|
-
throw new HarborError("
|
|
1418
|
+
throw new HarborError("Roadmaps need full access. Run harbor login, then try again.");
|
|
1361
1419
|
}
|
|
1362
1420
|
const r = await client.app({
|
|
1363
1421
|
op: "roadmap",
|
|
1364
1422
|
...(harbor ? { harbor } : {}),
|
|
1365
1423
|
project,
|
|
1424
|
+
...(values.publish ? { publish: true } : {}),
|
|
1366
1425
|
...(values.off ? { off: true } : {}),
|
|
1367
1426
|
...(values.dates || values["no-dates"] ? { showDates: Boolean(values.dates) } : {}),
|
|
1368
1427
|
...(values.site || values["no-site"] ? { onSite: Boolean(values.site) } : {}),
|
|
@@ -1374,7 +1433,9 @@ export async function run(argv, io = {}) {
|
|
|
1374
1433
|
if (!r.url) {
|
|
1375
1434
|
out(
|
|
1376
1435
|
plain(
|
|
1377
|
-
|
|
1436
|
+
values.off
|
|
1437
|
+
? `${r.project} isn't published any more. Its link now says the roadmap isn't published.`
|
|
1438
|
+
: `${r.project} isn't public. harbor roadmap "${r.project}" --publish publishes it.`,
|
|
1378
1439
|
),
|
|
1379
1440
|
);
|
|
1380
1441
|
return 0;
|
|
@@ -1382,7 +1443,7 @@ export async function run(argv, io = {}) {
|
|
|
1382
1443
|
const shows = r.showDates ? "titles and the day each shipped" : "titles only";
|
|
1383
1444
|
out(
|
|
1384
1445
|
plain(
|
|
1385
|
-
`${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.`,
|
|
1446
|
+
`${r.project}${values.publish ? " is public" : "'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.`,
|
|
1386
1447
|
),
|
|
1387
1448
|
);
|
|
1388
1449
|
return 0;
|
|
@@ -1500,10 +1561,13 @@ export async function run(argv, io = {}) {
|
|
|
1500
1561
|
);
|
|
1501
1562
|
}
|
|
1502
1563
|
case "brief": {
|
|
1503
|
-
const { positionals, values, harbor } = parse(spec(["say"], ["portfolio"]));
|
|
1564
|
+
const { positionals, values, harbor } = parse(spec(["say", "all"], ["portfolio"]));
|
|
1504
1565
|
none(positionals);
|
|
1505
1566
|
if (!values.say) {
|
|
1506
|
-
|
|
1567
|
+
const args = { harbor, portfolio: values.portfolio, ...(values.all ? { all: true } : {}) };
|
|
1568
|
+
return call("weekly_brief", args, (r) =>
|
|
1569
|
+
r.hidden ? `${r.text}\n\nharbor brief --all lists every one.` : r.text,
|
|
1570
|
+
);
|
|
1507
1571
|
}
|
|
1508
1572
|
const result = await client
|
|
1509
1573
|
.call("shipping_forecast", { harbor, portfolio: values.portfolio, at: localMinute() })
|
|
@@ -1511,8 +1575,13 @@ export async function run(argv, io = {}) {
|
|
|
1511
1575
|
if (err instanceof HarborError) throw new HarborError(hint(err.message));
|
|
1512
1576
|
throw err;
|
|
1513
1577
|
});
|
|
1578
|
+
// --json is for a script reading the forecast, so nothing is spoken.
|
|
1579
|
+
if (asJson) {
|
|
1580
|
+
out(jsonText(result));
|
|
1581
|
+
return 0;
|
|
1582
|
+
}
|
|
1514
1583
|
const script = plain(result.script ?? "");
|
|
1515
|
-
out(
|
|
1584
|
+
out(script);
|
|
1516
1585
|
if (!(await (io.speak ?? speak)(script))) {
|
|
1517
1586
|
throw new HarborError(
|
|
1518
1587
|
process.platform === "linux"
|
package/lib/format.mjs
CHANGED
|
@@ -63,12 +63,15 @@ export function myWork(w, opts) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export function taskList(r, opts) {
|
|
66
|
-
|
|
66
|
+
const done = r.doneMatches
|
|
67
|
+
? `${r.doneMatches} done task${r.doneMatches === 1 ? "" : "s"} match${r.doneMatches === 1 ? "es" : ""} too; add --status done to see ${r.doneMatches === 1 ? "it" : "them"}.`
|
|
68
|
+
: "";
|
|
69
|
+
if (!r.tasks?.length) return done ? `No open tasks match. ${done}` : "No tasks match.";
|
|
67
70
|
const more =
|
|
68
71
|
r.total > r.tasks.length
|
|
69
|
-
? `\n…and ${r.total - r.tasks.length} more.
|
|
72
|
+
? `\n…and ${r.total - r.tasks.length} more. Add --all for every one, or narrow it with a word or --project.`
|
|
70
73
|
: "";
|
|
71
|
-
return r.tasks.map((t) => taskLine(t, opts)).join("\n") + more;
|
|
74
|
+
return r.tasks.map((t) => taskLine(t, opts)).join("\n") + more + (done ? `\n${done}` : "");
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
const HEALTH = { "on-track": "on track", "at-risk": "at risk", "off-track": "off track" };
|
|
@@ -166,6 +169,28 @@ export function limitsText(name, limits, tasks = {}, hidden = []) {
|
|
|
166
169
|
return `${name}: ${capped.join(", ")}${rest}.${offNote}`;
|
|
167
170
|
}
|
|
168
171
|
|
|
172
|
+
/**
|
|
173
|
+
* harbor whoami: the account this machine signed in as, where, and the
|
|
174
|
+
* harbor commands use. From list_harbors (which names the account) and the
|
|
175
|
+
* saved sign-in.
|
|
176
|
+
*/
|
|
177
|
+
export function whoami(r, creds) {
|
|
178
|
+
const a = r.account;
|
|
179
|
+
const who = a?.name && a?.email ? `${a.name} (${a.email})` : (a?.email ?? a?.name ?? null);
|
|
180
|
+
const reading = creds.scope?.split(" ").includes("harbor:write") ? "" : ", reading only";
|
|
181
|
+
const lines = [`${who ? `Signed in as ${who}` : "Signed in"} to ${creds.server}${reading}.`];
|
|
182
|
+
const list = r.harbors ?? [];
|
|
183
|
+
const set = creds.harbor ? list.find((h) => h.id === creds.harbor) : null;
|
|
184
|
+
if (set) lines.push(`Commands use ${set.name} (harbor use).`);
|
|
185
|
+
else if (creds.harbor)
|
|
186
|
+
lines.push(`Commands use ${creds.harborName ?? creds.harbor}, which you're no longer in.`);
|
|
187
|
+
else if (list.length === 1) lines.push(`Commands use ${list[0].name}, your only harbor.`);
|
|
188
|
+
else if (list.length)
|
|
189
|
+
lines.push("No harbor set: say --harbor NAME, or set one with harbor use NAME.");
|
|
190
|
+
else lines.push("You aren't in any harbor yet. Run harbor to make one.");
|
|
191
|
+
return lines.join("\n");
|
|
192
|
+
}
|
|
193
|
+
|
|
169
194
|
export function harbors(r) {
|
|
170
195
|
const list = r.harbors ?? [];
|
|
171
196
|
if (!list.length) return "You aren't in any harbor yet. Run harbor to make one.";
|