@harborppm/cli 0.66.0 → 0.69.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 +1 -1
- package/dist/app.mjs +62 -107
- package/lib/cli.mjs +91 -10
- package/lib/format.mjs +1 -1
- package/package.json +1 -1
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.`;
|
|
@@ -6437,20 +6439,23 @@ function PriorityMark({ priority }) {
|
|
|
6437
6439
|
children: priority === "urgent" ? "!!" : priority === "high" ? "!" : "↓"
|
|
6438
6440
|
});
|
|
6439
6441
|
}
|
|
6442
|
+
/** A heading keeps its own width, up to this much of the row; the hints beside it shorten first. */
|
|
6443
|
+
const HEADING_MAX = "70%";
|
|
6440
6444
|
/** A section heading inside a screen. */
|
|
6441
6445
|
function Heading({ children, right }) {
|
|
6442
6446
|
return /* @__PURE__ */ jsxs(Box, {
|
|
6443
6447
|
justifyContent: "space-between",
|
|
6444
6448
|
columnGap: 2,
|
|
6445
6449
|
children: [/* @__PURE__ */ jsx(Box, {
|
|
6446
|
-
flexShrink:
|
|
6450
|
+
flexShrink: 0,
|
|
6451
|
+
maxWidth: "70%",
|
|
6447
6452
|
children: /* @__PURE__ */ jsx(Text, {
|
|
6448
6453
|
bold: true,
|
|
6449
6454
|
wrap: "truncate-end",
|
|
6450
6455
|
children
|
|
6451
6456
|
})
|
|
6452
6457
|
}), right ? /* @__PURE__ */ jsx(Box, {
|
|
6453
|
-
flexShrink:
|
|
6458
|
+
flexShrink: 1,
|
|
6454
6459
|
children: /* @__PURE__ */ jsx(Text, {
|
|
6455
6460
|
color: theme.muted,
|
|
6456
6461
|
wrap: "truncate-end",
|
|
@@ -9796,6 +9801,50 @@ var init_load = __esmMin((() => {
|
|
|
9796
9801
|
};
|
|
9797
9802
|
}));
|
|
9798
9803
|
|
|
9804
|
+
//#endregion
|
|
9805
|
+
//#region src/lib/first-voyage.ts
|
|
9806
|
+
function isVoyageProject(projectId) {
|
|
9807
|
+
return projectId === VOYAGE_PROJECT_ID;
|
|
9808
|
+
}
|
|
9809
|
+
var VOYAGE_PROJECT_ID, STEPS, voyageTaskId, SEEDED;
|
|
9810
|
+
var init_first_voyage = __esmMin((() => {
|
|
9811
|
+
VOYAGE_PROJECT_ID = "voyage";
|
|
9812
|
+
STEPS = [
|
|
9813
|
+
{
|
|
9814
|
+
step: "task",
|
|
9815
|
+
title: "Add a task of your own",
|
|
9816
|
+
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."
|
|
9817
|
+
},
|
|
9818
|
+
{
|
|
9819
|
+
step: "move",
|
|
9820
|
+
title: "Move a task to In progress",
|
|
9821
|
+
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."
|
|
9822
|
+
},
|
|
9823
|
+
{
|
|
9824
|
+
step: "crew",
|
|
9825
|
+
title: "Bring someone aboard, or tick this to skip",
|
|
9826
|
+
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."
|
|
9827
|
+
},
|
|
9828
|
+
{
|
|
9829
|
+
step: "terminal",
|
|
9830
|
+
title: "Try Harbor in your terminal",
|
|
9831
|
+
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."
|
|
9832
|
+
},
|
|
9833
|
+
{
|
|
9834
|
+
step: "agent",
|
|
9835
|
+
title: "Ask an AI app to add a task for you",
|
|
9836
|
+
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."
|
|
9837
|
+
},
|
|
9838
|
+
{
|
|
9839
|
+
step: "wrap",
|
|
9840
|
+
title: "Mark this project Done, or delete it",
|
|
9841
|
+
description: "That's the voyage. Set its health to Done to put it away, or delete it from Details. Your own work stays."
|
|
9842
|
+
}
|
|
9843
|
+
];
|
|
9844
|
+
voyageTaskId = (step) => `voyage-${step}`;
|
|
9845
|
+
SEEDED = new Set(STEPS.map((s) => voyageTaskId(s.step)));
|
|
9846
|
+
}));
|
|
9847
|
+
|
|
9799
9848
|
//#endregion
|
|
9800
9849
|
//#region src/lib/handover.ts
|
|
9801
9850
|
/** Everything a person carries that someone else would have to pick up, grouped in KIND_ORDER. */
|
|
@@ -10024,6 +10073,7 @@ var init_handover = __esmMin((() => {
|
|
|
10024
10073
|
init_date_fns();
|
|
10025
10074
|
init_indexes();
|
|
10026
10075
|
init_raid();
|
|
10076
|
+
init_first_voyage();
|
|
10027
10077
|
init_task_keys();
|
|
10028
10078
|
init_load();
|
|
10029
10079
|
init_date_text();
|
|
@@ -10084,6 +10134,7 @@ function lifeboats(data, now, opts = {}) {
|
|
|
10084
10134
|
const out = [];
|
|
10085
10135
|
for (const project of data.projects) {
|
|
10086
10136
|
if (project.health === "done" || project.health === "paused") continue;
|
|
10137
|
+
if (isVoyageProject(project.id)) continue;
|
|
10087
10138
|
if (!inScope(project.programId)) continue;
|
|
10088
10139
|
const tasks = grouped.get(project.id) ?? [];
|
|
10089
10140
|
const open = tasks.filter((t) => t.status !== "done");
|
|
@@ -19518,7 +19569,7 @@ var init_passage = __esmMin((() => {
|
|
|
19518
19569
|
isPassage = (r) => r.kind === "passage";
|
|
19519
19570
|
iso$1 = (d) => todayISO(d);
|
|
19520
19571
|
at = (day) => parseIso(`${day}T12:00:00`);
|
|
19521
|
-
byDue = (a, b) => (a.dueDate ?? "9999").localeCompare(b.dueDate ?? "9999")
|
|
19572
|
+
byDue = (a, b) => (a.dueDate ?? "9999").localeCompare(b.dueDate ?? "9999");
|
|
19522
19573
|
DAY_WORDS = [
|
|
19523
19574
|
"mon",
|
|
19524
19575
|
"tue",
|
|
@@ -21942,104 +21993,6 @@ var init_session = __esmMin((() => {
|
|
|
21942
21993
|
init_format();
|
|
21943
21994
|
}));
|
|
21944
21995
|
|
|
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
21996
|
//#endregion
|
|
22044
21997
|
//#region src/lib/first-week.ts
|
|
22045
21998
|
/** The program launch named for you, when nobody has renamed it yet. */
|
|
@@ -22108,7 +22061,7 @@ function hierarchySlice({ get, write }) {
|
|
|
22108
22061
|
get().log(`opened project ${row.name}`, `/projects/${id}`);
|
|
22109
22062
|
return id;
|
|
22110
22063
|
},
|
|
22111
|
-
addSampleProject: () => {
|
|
22064
|
+
addSampleProject: (sampleProject) => {
|
|
22112
22065
|
const state = get();
|
|
22113
22066
|
const program = state.programs[0];
|
|
22114
22067
|
if (!program) return null;
|
|
@@ -22483,7 +22436,6 @@ var init_hierarchy = __esmMin((() => {
|
|
|
22483
22436
|
init_shared();
|
|
22484
22437
|
init_hill();
|
|
22485
22438
|
init_charter_lines();
|
|
22486
|
-
init_sample_project();
|
|
22487
22439
|
init_format();
|
|
22488
22440
|
init_empty();
|
|
22489
22441
|
init_first_week();
|
|
@@ -26127,7 +26079,7 @@ function changesFrom(data, project, flags, now) {
|
|
|
26127
26079
|
});
|
|
26128
26080
|
return changes;
|
|
26129
26081
|
}
|
|
26130
|
-
const likely = (t) => t.kind === "forecast" ? t.likely ? shortDate(t.likely) : "not in 2 years" : t.kind === "done" ? "done" : "no pace yet";
|
|
26082
|
+
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
26083
|
const very = (t) => t.kind === "forecast" && t.veryLikely ? shortDate(t.veryLikely) : "-";
|
|
26132
26084
|
/** What `harbor try` prints: the changes, then the two plans side by side. */
|
|
26133
26085
|
function tryLines(data, project, changes, cmp, today) {
|
|
@@ -28026,7 +27978,8 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
|
|
|
28026
27978
|
justifyContent: "space-between",
|
|
28027
27979
|
columnGap: 2,
|
|
28028
27980
|
children: [/* @__PURE__ */ jsx(Box, {
|
|
28029
|
-
flexShrink:
|
|
27981
|
+
flexShrink: 0,
|
|
27982
|
+
maxWidth: "70%",
|
|
28030
27983
|
children: /* @__PURE__ */ jsxs(Text, {
|
|
28031
27984
|
wrap: "truncate-end",
|
|
28032
27985
|
children: [
|
|
@@ -28047,7 +28000,7 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
|
|
|
28047
28000
|
]
|
|
28048
28001
|
})
|
|
28049
28002
|
}), /* @__PURE__ */ jsx(Box, {
|
|
28050
|
-
flexShrink:
|
|
28003
|
+
flexShrink: 1,
|
|
28051
28004
|
children: /* @__PURE__ */ jsxs(Text, {
|
|
28052
28005
|
color: theme.muted,
|
|
28053
28006
|
wrap: "truncate-end",
|
|
@@ -28141,7 +28094,9 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
|
|
|
28141
28094
|
const w = Math.floor((width - 2) / shownColumns.length);
|
|
28142
28095
|
const on = active && ci === col;
|
|
28143
28096
|
const r = Math.min(rowByCol[ci] ?? 0, Math.max(0, c.tasks.length - 1));
|
|
28144
|
-
const
|
|
28097
|
+
const room = body - 3;
|
|
28098
|
+
const fits = Math.floor(room / 3) >= c.tasks.length;
|
|
28099
|
+
const view = windowOf(c.tasks, r, Math.max(1, Math.floor((fits ? room : room - 2) / 3)));
|
|
28145
28100
|
const over = c.limit != null && c.all > c.limit;
|
|
28146
28101
|
return /* @__PURE__ */ jsxs(Box, {
|
|
28147
28102
|
flexDirection: "column",
|
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;
|
|
@@ -415,11 +440,28 @@ const level = (v, name) => {
|
|
|
415
440
|
const nullable = (v) => (v === "none" ? null : v);
|
|
416
441
|
const REPEAT_WORD = { weekly: "every week", biweekly: "every two weeks", monthly: "every month" };
|
|
417
442
|
|
|
418
|
-
|
|
443
|
+
const WEEKDAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
|
|
444
|
+
|
|
445
|
+
/** "fri", "Friday": how many days until that weekday comes round (today counts). */
|
|
446
|
+
function daysToWeekday(v, today) {
|
|
447
|
+
const w = v.toLowerCase();
|
|
448
|
+
if (w.length < 3) return null;
|
|
449
|
+
const i = WEEKDAYS.findIndex((d) => d.startsWith(w));
|
|
450
|
+
return i < 0 ? null : (i - today.getDay() + 7) % 7;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** --due as people say it: today, tomorrow, friday, +3, or a date. Anything else goes to Harbor to judge. */
|
|
419
454
|
export function dueDate(v, today = new Date()) {
|
|
420
455
|
if (v === undefined || v === null) return v;
|
|
421
456
|
const words = { today: 0, tomorrow: 1 };
|
|
422
|
-
const days =
|
|
457
|
+
const days =
|
|
458
|
+
v in words
|
|
459
|
+
? words[v]
|
|
460
|
+
: /^\+\d{1,3}$/.test(v)
|
|
461
|
+
? Number(v.slice(1))
|
|
462
|
+
: /^[a-z]+$/i.test(v)
|
|
463
|
+
? daysToWeekday(v, today)
|
|
464
|
+
: null;
|
|
423
465
|
if (days === null) return v;
|
|
424
466
|
const d = new Date(today.getFullYear(), today.getMonth(), today.getDate() + days);
|
|
425
467
|
return todayLocal(d);
|
|
@@ -513,7 +555,7 @@ export function hint(message, project) {
|
|
|
513
555
|
(_, list) => `pick one of ${list.replace(/"/g, "").split("|").join(", ")}`,
|
|
514
556
|
)
|
|
515
557
|
// The CLI reads today, tomorrow and +N before Harbor sees the date.
|
|
516
|
-
.replace(/^a date as YYYY-MM-DD$/, "a date like 2026-11-30, today, tomorrow or +N days");
|
|
558
|
+
.replace(/^a date as YYYY-MM-DD$/, "a date like 2026-11-30, today, tomorrow, friday or +N days");
|
|
517
559
|
if (!seen.has(flag)) seen.set(flag, said);
|
|
518
560
|
}
|
|
519
561
|
return [...seen].map(([flag, why]) => `${flag}: ${why}`).join("\n");
|
|
@@ -780,7 +822,9 @@ const DETAIL = {
|
|
|
780
822
|
" names them",
|
|
781
823
|
"",
|
|
782
824
|
" FILE is CSV or tab-separated: a spreadsheet, or a Jira, Asana or Linear",
|
|
783
|
-
" export.
|
|
825
|
+
" export. A Trello board's JSON export works too (Menu, then Print, export",
|
|
826
|
+
" and share, then Export as JSON): each list becomes a status, and the",
|
|
827
|
+
" board a project. It needs a column of titles; rows naming a Project go",
|
|
784
828
|
" there. People it names who aren't in the harbor are added unless you say",
|
|
785
829
|
" --no-people. A task already in its project is left alone: by its key",
|
|
786
830
|
" (a Key column, as harbor export writes) or by title.",
|
|
@@ -1133,6 +1177,22 @@ const DETAIL = {
|
|
|
1133
1177
|
],
|
|
1134
1178
|
};
|
|
1135
1179
|
|
|
1180
|
+
/**
|
|
1181
|
+
* A Trello board's JSON export without its activity log, which is most of the
|
|
1182
|
+
* file and none of what an import reads; anything else is sent as it is.
|
|
1183
|
+
*/
|
|
1184
|
+
export function trelloLean(text) {
|
|
1185
|
+
if (!text.trimStart().startsWith("{")) return text;
|
|
1186
|
+
try {
|
|
1187
|
+
const board = JSON.parse(text);
|
|
1188
|
+
if (!Array.isArray(board?.cards) || !Array.isArray(board?.lists)) return text;
|
|
1189
|
+
const { name, cards, lists, members } = board;
|
|
1190
|
+
return JSON.stringify({ name, cards, lists, members });
|
|
1191
|
+
} catch {
|
|
1192
|
+
return text;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1136
1196
|
/** The lines of the help about one command, or null when it has none. */
|
|
1137
1197
|
export function commandHelp(command) {
|
|
1138
1198
|
const lines = HELP.split("\n");
|
|
@@ -1183,12 +1243,28 @@ function distance(a, b) {
|
|
|
1183
1243
|
return d[a.length][b.length];
|
|
1184
1244
|
}
|
|
1185
1245
|
|
|
1246
|
+
/** What someone who knows the website, or a feature's name, might type. */
|
|
1247
|
+
const OTHER_NAMES = {
|
|
1248
|
+
today: "harbor mine lists what's on you, late first; harbor week plans your days.",
|
|
1249
|
+
inbox: "harbor catchup says what was handed to you and what moved.",
|
|
1250
|
+
people: "harbor lifeboats, harbor clock and harbor ashore cover the crew; the app's People screen (harbor, then 6) shows everyone's load.",
|
|
1251
|
+
crew: "harbor invite EMAIL brings someone aboard; the app's Crew screen (harbor, then C) lists who's on it.",
|
|
1252
|
+
longpole: "harbor why PROJECT shows what's holding it up.",
|
|
1253
|
+
"long-pole": "harbor why PROJECT shows what's holding it up.",
|
|
1254
|
+
pilot: "harbor show TASK says the date its owner's queue suggests; --due pilot on add or edit takes it.",
|
|
1255
|
+
dock: "harbor try PROJECT is the dry dock.",
|
|
1256
|
+
"dry-dock": "harbor try PROJECT is the dry dock.",
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1186
1259
|
/** A mistyped command, with the one it was probably meant to be. */
|
|
1187
1260
|
export function noCommand(name) {
|
|
1188
1261
|
if (name.startsWith("-")) return `${name} goes after a command, like harbor mine ${name}.`;
|
|
1189
1262
|
// A plural kind reads the register, not the command that writes one.
|
|
1190
1263
|
if (["decisions", "issues"].includes(name))
|
|
1191
1264
|
return `No command "${name}". harbor risks --kind ${name.slice(0, -1)} lists them.`;
|
|
1265
|
+
// Words for things the website has a page for, or that go by another name here.
|
|
1266
|
+
const other = OTHER_NAMES[name.toLowerCase()];
|
|
1267
|
+
if (other) return `No command "${name}". ${other}`;
|
|
1192
1268
|
const near = COMMANDS.filter((c) => distance(name, c) <= (c.length > 4 ? 2 : 1));
|
|
1193
1269
|
const guess = near.length === 1 ? ` Did you mean harbor ${near[0]}?` : "";
|
|
1194
1270
|
return `No command "${name}".${guess} Run harbor help.`;
|
|
@@ -1266,7 +1342,8 @@ export async function run(argv, io = {}) {
|
|
|
1266
1342
|
|
|
1267
1343
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
1268
1344
|
// `harbor help mine`: that command's help, as `harbor mine --help` gives it.
|
|
1269
|
-
|
|
1345
|
+
const all = rest[0] === "all" || rest[0] === "help" || rest.includes("--all");
|
|
1346
|
+
if (rest[0] && !all) {
|
|
1270
1347
|
const help = commandHelp(rest[0]);
|
|
1271
1348
|
if (help === null) throw new HarborError(noCommand(rest[0]));
|
|
1272
1349
|
out(help);
|
|
@@ -1280,7 +1357,8 @@ export async function run(argv, io = {}) {
|
|
|
1280
1357
|
dim(`cli ${VERSION} · harborppm.com`),
|
|
1281
1358
|
]);
|
|
1282
1359
|
// The banner says what the help's first line does.
|
|
1283
|
-
|
|
1360
|
+
const text = all ? HELP : START_HELP;
|
|
1361
|
+
out(top ? `${top}${text.slice(text.indexOf("\n"))}` : text);
|
|
1284
1362
|
return 0;
|
|
1285
1363
|
}
|
|
1286
1364
|
|
|
@@ -1295,7 +1373,7 @@ export async function run(argv, io = {}) {
|
|
|
1295
1373
|
if (help === null && command !== "app") {
|
|
1296
1374
|
throw new HarborError(noCommand(command));
|
|
1297
1375
|
}
|
|
1298
|
-
out(help ??
|
|
1376
|
+
out(help ?? START_HELP);
|
|
1299
1377
|
return 0;
|
|
1300
1378
|
}
|
|
1301
1379
|
|
|
@@ -1309,7 +1387,7 @@ export async function run(argv, io = {}) {
|
|
|
1309
1387
|
if (!command || command === "app" || bridgeApp) {
|
|
1310
1388
|
const terminal = inTerminal;
|
|
1311
1389
|
if (!command && !terminal) {
|
|
1312
|
-
out(
|
|
1390
|
+
out(START_HELP);
|
|
1313
1391
|
return 0;
|
|
1314
1392
|
}
|
|
1315
1393
|
if (!terminal) throw new HarborError("The full-screen app needs a terminal.");
|
|
@@ -1596,7 +1674,7 @@ export async function run(argv, io = {}) {
|
|
|
1596
1674
|
veryLikely: f?.veryLikely ?? null,
|
|
1597
1675
|
chanceByEnd: f?.chanceByEnd ?? null,
|
|
1598
1676
|
tasksPerWeek: f?.perWeek ?? null,
|
|
1599
|
-
// Why there are no dates, when there aren't: no pace yet, paused, done.
|
|
1677
|
+
// Why there are no dates, when there aren't: no tasks yet, no pace yet, paused, done.
|
|
1600
1678
|
noForecast: f ? null : s.tide.kind,
|
|
1601
1679
|
};
|
|
1602
1680
|
};
|
|
@@ -2309,7 +2387,9 @@ export async function run(argv, io = {}) {
|
|
|
2309
2387
|
added.push(r);
|
|
2310
2388
|
}
|
|
2311
2389
|
await stalePromptCache(env);
|
|
2312
|
-
|
|
2390
|
+
// `added` is always the list, so a script reads one key however many
|
|
2391
|
+
// titles it sent; one title also keeps its `created` at the top.
|
|
2392
|
+
if (asJson) out(jsonText(added.length === 1 ? { ...added[0], added } : { added }));
|
|
2313
2393
|
else
|
|
2314
2394
|
out(
|
|
2315
2395
|
plain(
|
|
@@ -2642,6 +2722,7 @@ export async function run(argv, io = {}) {
|
|
|
2642
2722
|
} catch {
|
|
2643
2723
|
throw new HarborError(`Can't read ${file}.`);
|
|
2644
2724
|
}
|
|
2725
|
+
csv = trelloLean(csv);
|
|
2645
2726
|
return call(
|
|
2646
2727
|
"import_tasks",
|
|
2647
2728
|
{
|
package/lib/format.mjs
CHANGED