@harborppm/cli 0.66.0 → 0.68.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
@@ -46,7 +46,7 @@ It runs on the same engine as the website: health, task keys, limits and undo wo
46
46
 
47
47
  It has colours for dark terminals and for light ones, and picks by asking the terminal for its background (or reading `COLORFGBG`); a terminal that doesn't say gets the dark ones. `harbor theme light` or `harbor theme dark` settles it, `harbor theme auto` goes back to asking, and `HARBOR_THEME` overrides it for one run.
48
48
 
49
- `harbor help` shows the mark too, in a terminal with colour; piped, or with `NO_COLOR` set, it's plain text. `harbor --version` says which version you have.
49
+ `harbor help` starts with the dozen commands most people need; `harbor help all` lists every one. It shows the mark too, in a terminal with colour; piped, or with `NO_COLOR` set, it's plain text. `harbor --version` says which version you have.
50
50
 
51
51
  ## On the website
52
52
 
package/dist/app.mjs CHANGED
@@ -3128,6 +3128,7 @@ function tideOptions(data) {
3128
3128
  * "what if Sam joined" (src/lib/dry-dock.ts).
3129
3129
  */
3130
3130
  function tideForecast(project, tasks, now, opts = {}) {
3131
+ if (tasks.length === 0) return { kind: "empty" };
3131
3132
  const remaining = tasks.filter((t) => t.status !== "done").length;
3132
3133
  if (remaining === 0) return { kind: "done" };
3133
3134
  if (project.health === "paused") return {
@@ -3201,6 +3202,7 @@ function dueAfterFinish(tide) {
3201
3202
  /** Why there's no forecast, for the states without one. */
3202
3203
  function tideQuiet(tide) {
3203
3204
  switch (tide.kind) {
3205
+ case "empty": return "No tasks yet, so there's nothing to forecast.";
3204
3206
  case "done": return "Every task is done.";
3205
3207
  case "paused": return "Paused, so its pace says nothing about when it ends.";
3206
3208
  case "no-pace": return `Nothing finished in the last ${6} weeks, so there's no pace to forecast from yet.`;
@@ -9796,6 +9798,50 @@ var init_load = __esmMin((() => {
9796
9798
  };
9797
9799
  }));
9798
9800
 
9801
+ //#endregion
9802
+ //#region src/lib/first-voyage.ts
9803
+ function isVoyageProject(projectId) {
9804
+ return projectId === VOYAGE_PROJECT_ID;
9805
+ }
9806
+ var VOYAGE_PROJECT_ID, STEPS, voyageTaskId, SEEDED;
9807
+ var init_first_voyage = __esmMin((() => {
9808
+ VOYAGE_PROJECT_ID = "voyage";
9809
+ STEPS = [
9810
+ {
9811
+ step: "task",
9812
+ title: "Add a task of your own",
9813
+ description: "Press New, or the + at the top of a column on any board. Anything real will do. This ticks itself off when you add one."
9814
+ },
9815
+ {
9816
+ step: "move",
9817
+ title: "Move a task to In progress",
9818
+ description: "Open Board above and drag a card along, or change its status in the task's panel. This one counts too. It ticks itself off."
9819
+ },
9820
+ {
9821
+ step: "crew",
9822
+ title: "Bring someone aboard, or tick this to skip",
9823
+ description: "Invite from your menu or from People. Harbor is $49 a month for the whole crew, not per seat. Ticks itself off when someone's in People."
9824
+ },
9825
+ {
9826
+ step: "terminal",
9827
+ title: "Try Harbor in your terminal",
9828
+ description: "Run `npx @harborppm/cli mine` (Node 22 or later) and sign in when your browser asks. `harbor` on its own opens the whole harbor full screen. Ticks itself off once the terminal is connected; skip it if a terminal isn't your thing."
9829
+ },
9830
+ {
9831
+ step: "agent",
9832
+ title: "Ask an AI app to add a task for you",
9833
+ description: "Connect Claude, or any app that speaks MCP, at harborppm.com/ai, then ask it to add a task here. Harbor asks you first and History names the app. Ticks itself off once an app is connected."
9834
+ },
9835
+ {
9836
+ step: "wrap",
9837
+ title: "Mark this project Done, or delete it",
9838
+ description: "That's the voyage. Set its health to Done to put it away, or delete it from Details. Your own work stays."
9839
+ }
9840
+ ];
9841
+ voyageTaskId = (step) => `voyage-${step}`;
9842
+ SEEDED = new Set(STEPS.map((s) => voyageTaskId(s.step)));
9843
+ }));
9844
+
9799
9845
  //#endregion
9800
9846
  //#region src/lib/handover.ts
9801
9847
  /** Everything a person carries that someone else would have to pick up, grouped in KIND_ORDER. */
@@ -10024,6 +10070,7 @@ var init_handover = __esmMin((() => {
10024
10070
  init_date_fns();
10025
10071
  init_indexes();
10026
10072
  init_raid();
10073
+ init_first_voyage();
10027
10074
  init_task_keys();
10028
10075
  init_load();
10029
10076
  init_date_text();
@@ -10084,6 +10131,7 @@ function lifeboats(data, now, opts = {}) {
10084
10131
  const out = [];
10085
10132
  for (const project of data.projects) {
10086
10133
  if (project.health === "done" || project.health === "paused") continue;
10134
+ if (isVoyageProject(project.id)) continue;
10087
10135
  if (!inScope(project.programId)) continue;
10088
10136
  const tasks = grouped.get(project.id) ?? [];
10089
10137
  const open = tasks.filter((t) => t.status !== "done");
@@ -19518,7 +19566,7 @@ var init_passage = __esmMin((() => {
19518
19566
  isPassage = (r) => r.kind === "passage";
19519
19567
  iso$1 = (d) => todayISO(d);
19520
19568
  at = (day) => parseIso(`${day}T12:00:00`);
19521
- byDue = (a, b) => (a.dueDate ?? "9999").localeCompare(b.dueDate ?? "9999") || a.title.localeCompare(b.title);
19569
+ byDue = (a, b) => (a.dueDate ?? "9999").localeCompare(b.dueDate ?? "9999");
19522
19570
  DAY_WORDS = [
19523
19571
  "mon",
19524
19572
  "tue",
@@ -21942,104 +21990,6 @@ var init_session = __esmMin((() => {
21942
21990
  init_format();
21943
21991
  }));
21944
21992
 
21945
- //#endregion
21946
- //#region src/lib/sample-project.ts
21947
- function sampleProject(opts) {
21948
- const { ownerId, today, now, makeId } = opts;
21949
- const day = (n) => todayISO(addDays(parseDate$1(today), n));
21950
- const end = opts.programEnd && opts.programEnd < day(30) ? opts.programEnd : day(30);
21951
- const projectId = makeId("pj");
21952
- const project = {
21953
- id: projectId,
21954
- programId: opts.programId,
21955
- name: SAMPLE_PROJECT_NAME,
21956
- description: "A project to try things on. Move cards, add tasks, change dates. Delete it from Details when you’re done.",
21957
- swatch: "sea",
21958
- ownerId,
21959
- health: "on-track",
21960
- startDate: today,
21961
- endDate: end,
21962
- budget: 0,
21963
- spent: 0
21964
- };
21965
- const task = (title, status, o = {}) => ({
21966
- id: makeId("t"),
21967
- projectId,
21968
- title,
21969
- description: o.description ?? "",
21970
- status,
21971
- priority: o.priority ?? "medium",
21972
- assigneeId: ownerId,
21973
- dueDate: o.due == null ? null : day(o.due),
21974
- createdAt: now,
21975
- completedAt: status === "done" ? now : null,
21976
- checklist: (o.checklist ?? []).map(([t, done]) => ({
21977
- id: makeId("ck"),
21978
- title: t,
21979
- done
21980
- })),
21981
- blockedById: o.waitsOn ?? null,
21982
- blockedByIds: o.waitsOn ? [o.waitsOn] : []
21983
- });
21984
- const draft = task("Draft the new home page", "doing", {
21985
- description: "A sample card. Tick the checklist, leave a comment, or drag it to Review on the board.",
21986
- due: 3,
21987
- priority: "high",
21988
- checklist: [
21989
- ["Headline and one line under it", true],
21990
- ["Three reasons to stay", false],
21991
- ["One clear button", false]
21992
- ]
21993
- });
21994
- return {
21995
- project,
21996
- tasks: [
21997
- task("Agree what the refresh is for", "done", { description: "Finished. Move it back if it turns out it wasn’t." }),
21998
- task("List the pages people actually use", "done"),
21999
- draft,
22000
- task("Write the pricing page copy", "review", {
22001
- description: "Waiting for a second pair of eyes. Move it to Done when it reads well.",
22002
- due: 5
22003
- }),
22004
- task("Pick the photos", "todo", {
22005
- description: "Hand this one to someone else from the card’s Owner field.",
22006
- due: 7
22007
- }),
22008
- task("Check every page on a phone", "todo", {
22009
- description: "Waits on the home page draft, so it shows as waiting until that card is done.",
22010
- due: 10,
22011
- waitsOn: draft.id
22012
- }),
22013
- task("Send old links to the new pages", "backlog", { description: "No date yet. Backlog is for what isn’t planned." }),
22014
- task("Tell customers what changed", "backlog", { priority: "low" })
22015
- ],
22016
- raidItems: [{
22017
- id: makeId("r"),
22018
- kind: "risk",
22019
- projectId,
22020
- title: "Photos may not be cleared in time",
22021
- description: "A sample risk. If the photos aren’t licensed before launch, the home page ships with placeholders.",
22022
- severity: "medium",
22023
- status: "open",
22024
- ownerId,
22025
- dueDate: day(7),
22026
- probability: 3,
22027
- impact: 3,
22028
- mitigation: "Shortlist stock photos we already own as a fallback.",
22029
- createdById: ownerId,
22030
- createdAt: now,
22031
- updatedAt: now,
22032
- updates: []
22033
- }]
22034
- };
22035
- }
22036
- var SAMPLE_PROJECT_NAME;
22037
- var init_sample_project = __esmMin((() => {
22038
- init_date_fns();
22039
- init_format();
22040
- SAMPLE_PROJECT_NAME = "Website refresh (sample)";
22041
- }));
22042
-
22043
21993
  //#endregion
22044
21994
  //#region src/lib/first-week.ts
22045
21995
  /** The program launch named for you, when nobody has renamed it yet. */
@@ -22108,7 +22058,7 @@ function hierarchySlice({ get, write }) {
22108
22058
  get().log(`opened project ${row.name}`, `/projects/${id}`);
22109
22059
  return id;
22110
22060
  },
22111
- addSampleProject: () => {
22061
+ addSampleProject: (sampleProject) => {
22112
22062
  const state = get();
22113
22063
  const program = state.programs[0];
22114
22064
  if (!program) return null;
@@ -22483,7 +22433,6 @@ var init_hierarchy = __esmMin((() => {
22483
22433
  init_shared();
22484
22434
  init_hill();
22485
22435
  init_charter_lines();
22486
- init_sample_project();
22487
22436
  init_format();
22488
22437
  init_empty();
22489
22438
  init_first_week();
@@ -26127,7 +26076,7 @@ function changesFrom(data, project, flags, now) {
26127
26076
  });
26128
26077
  return changes;
26129
26078
  }
26130
- const likely = (t) => t.kind === "forecast" ? t.likely ? shortDate(t.likely) : "not in 2 years" : t.kind === "done" ? "done" : "no pace yet";
26079
+ const likely = (t) => t.kind === "forecast" ? t.likely ? shortDate(t.likely) : "not in 2 years" : t.kind === "done" ? "done" : t.kind === "empty" ? "no tasks yet" : "no pace yet";
26131
26080
  const very = (t) => t.kind === "forecast" && t.veryLikely ? shortDate(t.veryLikely) : "-";
26132
26081
  /** What `harbor try` prints: the changes, then the two plans side by side. */
26133
26082
  function tryLines(data, project, changes, cmp, today) {
package/lib/cli.mjs CHANGED
@@ -286,6 +286,31 @@ export const HELP = `harbor: Harbor from the terminal
286
286
  and so can harbor fog --sweep's x (drop it), one task at a time.
287
287
  `;
288
288
 
289
+ /**
290
+ * What `harbor help` shows first: the dozen commands a newcomer needs, then
291
+ * where the rest are. `harbor help all` is the whole list (HELP).
292
+ */
293
+ export const START_HELP = `harbor: Harbor from the terminal
294
+
295
+ harbor the full-screen app, by keyboard
296
+ harbor signup new to Harbor: make an account
297
+ harbor login sign in (opens your browser once)
298
+
299
+ harbor mine what's on you, late first
300
+ harbor add "TITLE" add a task (--project P, --due DATE)
301
+ harbor start TASK move a task to In play (TASK: a key like RR-42)
302
+ harbor done TASK finish it
303
+ harbor tasks [words] find tasks across the harbor
304
+ harbor projects projects with health and open work
305
+ harbor brief one letter: late work, live risks, what shipped
306
+ harbor open [TASK|PROJECT] open it on the website
307
+ harbor invite EMAIL invite someone to your harbor
308
+
309
+ harbor help all every command (forecasts, risks, weekly plans,
310
+ plans kept in a repo, and more)
311
+ harbor help COMMAND just that one, with all its options
312
+ `;
313
+
289
314
  export const VERSION = JSON.parse(
290
315
  readFileSync(new URL("../package.json", import.meta.url), "utf8"),
291
316
  ).version;
@@ -780,7 +805,9 @@ const DETAIL = {
780
805
  " names them",
781
806
  "",
782
807
  " FILE is CSV or tab-separated: a spreadsheet, or a Jira, Asana or Linear",
783
- " export. It needs a column of titles; rows naming a Project column go",
808
+ " export. A Trello board's JSON export works too (Menu, then Print, export",
809
+ " and share, then Export as JSON): each list becomes a status, and the",
810
+ " board a project. It needs a column of titles; rows naming a Project go",
784
811
  " there. People it names who aren't in the harbor are added unless you say",
785
812
  " --no-people. A task already in its project is left alone: by its key",
786
813
  " (a Key column, as harbor export writes) or by title.",
@@ -1133,6 +1160,22 @@ const DETAIL = {
1133
1160
  ],
1134
1161
  };
1135
1162
 
1163
+ /**
1164
+ * A Trello board's JSON export without its activity log, which is most of the
1165
+ * file and none of what an import reads; anything else is sent as it is.
1166
+ */
1167
+ export function trelloLean(text) {
1168
+ if (!text.trimStart().startsWith("{")) return text;
1169
+ try {
1170
+ const board = JSON.parse(text);
1171
+ if (!Array.isArray(board?.cards) || !Array.isArray(board?.lists)) return text;
1172
+ const { name, cards, lists, members } = board;
1173
+ return JSON.stringify({ name, cards, lists, members });
1174
+ } catch {
1175
+ return text;
1176
+ }
1177
+ }
1178
+
1136
1179
  /** The lines of the help about one command, or null when it has none. */
1137
1180
  export function commandHelp(command) {
1138
1181
  const lines = HELP.split("\n");
@@ -1266,7 +1309,8 @@ export async function run(argv, io = {}) {
1266
1309
 
1267
1310
  if (command === "help" || command === "--help" || command === "-h") {
1268
1311
  // `harbor help mine`: that command's help, as `harbor mine --help` gives it.
1269
- if (rest[0] && rest[0] !== "help") {
1312
+ const all = rest[0] === "all" || rest[0] === "help" || rest.includes("--all");
1313
+ if (rest[0] && !all) {
1270
1314
  const help = commandHelp(rest[0]);
1271
1315
  if (help === null) throw new HarborError(noCommand(rest[0]));
1272
1316
  out(help);
@@ -1280,7 +1324,8 @@ export async function run(argv, io = {}) {
1280
1324
  dim(`cli ${VERSION} · harborppm.com`),
1281
1325
  ]);
1282
1326
  // The banner says what the help's first line does.
1283
- out(top ? `${top}${HELP.slice(HELP.indexOf("\n"))}` : HELP);
1327
+ const text = all ? HELP : START_HELP;
1328
+ out(top ? `${top}${text.slice(text.indexOf("\n"))}` : text);
1284
1329
  return 0;
1285
1330
  }
1286
1331
 
@@ -1295,7 +1340,7 @@ export async function run(argv, io = {}) {
1295
1340
  if (help === null && command !== "app") {
1296
1341
  throw new HarborError(noCommand(command));
1297
1342
  }
1298
- out(help ?? HELP);
1343
+ out(help ?? START_HELP);
1299
1344
  return 0;
1300
1345
  }
1301
1346
 
@@ -1309,7 +1354,7 @@ export async function run(argv, io = {}) {
1309
1354
  if (!command || command === "app" || bridgeApp) {
1310
1355
  const terminal = inTerminal;
1311
1356
  if (!command && !terminal) {
1312
- out(HELP);
1357
+ out(START_HELP);
1313
1358
  return 0;
1314
1359
  }
1315
1360
  if (!terminal) throw new HarborError("The full-screen app needs a terminal.");
@@ -1596,7 +1641,7 @@ export async function run(argv, io = {}) {
1596
1641
  veryLikely: f?.veryLikely ?? null,
1597
1642
  chanceByEnd: f?.chanceByEnd ?? null,
1598
1643
  tasksPerWeek: f?.perWeek ?? null,
1599
- // Why there are no dates, when there aren't: no pace yet, paused, done.
1644
+ // Why there are no dates, when there aren't: no tasks yet, no pace yet, paused, done.
1600
1645
  noForecast: f ? null : s.tide.kind,
1601
1646
  };
1602
1647
  };
@@ -2309,7 +2354,9 @@ export async function run(argv, io = {}) {
2309
2354
  added.push(r);
2310
2355
  }
2311
2356
  await stalePromptCache(env);
2312
- if (asJson) out(jsonText(added.length === 1 ? added[0] : { added }));
2357
+ // `added` is always the list, so a script reads one key however many
2358
+ // titles it sent; one title also keeps its `created` at the top.
2359
+ if (asJson) out(jsonText(added.length === 1 ? { ...added[0], added } : { added }));
2313
2360
  else
2314
2361
  out(
2315
2362
  plain(
@@ -2642,6 +2689,7 @@ export async function run(argv, io = {}) {
2642
2689
  } catch {
2643
2690
  throw new HarborError(`Can't read ${file}.`);
2644
2691
  }
2692
+ csv = trelloLean(csv);
2645
2693
  return call(
2646
2694
  "import_tasks",
2647
2695
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.66.0",
3
+ "version": "0.68.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",