@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 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"` 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.
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 [--dates] [--off] publish a project's public roadmap
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] the weekly brief, ready to send
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, 8)) lines.push(`• ${t.title}${t.project ? ` (${t.project})` : ""}`);
5085
- if (opts.shipped.length > 8) lines.push(`• and ${opts.shipped.length - 8} more`);
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, 8)) {
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 > 8) lines.push(`• and ${opts.late.length - 8} more`);
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] [--off]
89
- publish a project's public roadmap
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] the weekly brief, ready to send;
124
- --say reads it aloud as a
125
- shipping forecast
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("Publishing needs full access. Run harbor login, then try again.");
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
- `${r.project} isn't published any more. Its link says the roadmap isn't published.`,
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
- return call("weekly_brief", { harbor, portfolio: values.portfolio }, (r) => r.text);
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(asJson ? jsonText(result) : script);
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
- if (!r.tasks?.length) return "No tasks match.";
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. Narrow it with a word or --project.`
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.";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.38.0",
3
+ "version": "0.40.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",