@harborppm/cli 0.65.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 +2 -2
- package/dist/app.mjs +54 -103
- package/lib/cli.mjs +56 -8
- package/lib/format.mjs +8 -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
|
|
|
@@ -189,7 +189,7 @@ harbor review [WORDS] [--mine|--owner NAME] [--portfolio P] [--done]
|
|
|
189
189
|
the review: programs worst first, with next moves
|
|
190
190
|
(reads only; harbor submit moves a task to In review)
|
|
191
191
|
harbor thanks [TASK "LINE"] [--to NAME] a one-line thanks for a task (--milestone, --decision);
|
|
192
|
-
on its own, the thanks you were given
|
|
192
|
+
on its own, the thanks you were given in the last 30 days
|
|
193
193
|
harbor bridge run the review as a meeting: one project a screen,
|
|
194
194
|
what changed since the last one, log decisions,
|
|
195
195
|
actions and risks live, post the summary to chat
|
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.`;
|
|
@@ -3285,6 +3287,7 @@ function barnaclesOf(project, tasks, people, now, slip = null) {
|
|
|
3285
3287
|
return {
|
|
3286
3288
|
since,
|
|
3287
3289
|
from,
|
|
3290
|
+
settling: from === "start" && since.slice(0, 10) > today,
|
|
3288
3291
|
planned,
|
|
3289
3292
|
added: added.length,
|
|
3290
3293
|
share: planned > 0 ? added.length / planned : null,
|
|
@@ -9795,6 +9798,50 @@ var init_load = __esmMin((() => {
|
|
|
9795
9798
|
};
|
|
9796
9799
|
}));
|
|
9797
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
|
+
|
|
9798
9845
|
//#endregion
|
|
9799
9846
|
//#region src/lib/handover.ts
|
|
9800
9847
|
/** Everything a person carries that someone else would have to pick up, grouped in KIND_ORDER. */
|
|
@@ -10023,6 +10070,7 @@ var init_handover = __esmMin((() => {
|
|
|
10023
10070
|
init_date_fns();
|
|
10024
10071
|
init_indexes();
|
|
10025
10072
|
init_raid();
|
|
10073
|
+
init_first_voyage();
|
|
10026
10074
|
init_task_keys();
|
|
10027
10075
|
init_load();
|
|
10028
10076
|
init_date_text();
|
|
@@ -10083,6 +10131,7 @@ function lifeboats(data, now, opts = {}) {
|
|
|
10083
10131
|
const out = [];
|
|
10084
10132
|
for (const project of data.projects) {
|
|
10085
10133
|
if (project.health === "done" || project.health === "paused") continue;
|
|
10134
|
+
if (isVoyageProject(project.id)) continue;
|
|
10086
10135
|
if (!inScope(project.programId)) continue;
|
|
10087
10136
|
const tasks = grouped.get(project.id) ?? [];
|
|
10088
10137
|
const open = tasks.filter((t) => t.status !== "done");
|
|
@@ -19517,7 +19566,7 @@ var init_passage = __esmMin((() => {
|
|
|
19517
19566
|
isPassage = (r) => r.kind === "passage";
|
|
19518
19567
|
iso$1 = (d) => todayISO(d);
|
|
19519
19568
|
at = (day) => parseIso(`${day}T12:00:00`);
|
|
19520
|
-
byDue = (a, b) => (a.dueDate ?? "9999").localeCompare(b.dueDate ?? "9999")
|
|
19569
|
+
byDue = (a, b) => (a.dueDate ?? "9999").localeCompare(b.dueDate ?? "9999");
|
|
19521
19570
|
DAY_WORDS = [
|
|
19522
19571
|
"mon",
|
|
19523
19572
|
"tue",
|
|
@@ -19581,6 +19630,7 @@ function barnaclesData(b) {
|
|
|
19581
19630
|
return {
|
|
19582
19631
|
measuredFrom: b.from === "baseline" ? "when the baseline was set" : "a week after its first task was made",
|
|
19583
19632
|
since: b.since,
|
|
19633
|
+
settling: b.settling,
|
|
19584
19634
|
planned: b.planned,
|
|
19585
19635
|
added: b.added,
|
|
19586
19636
|
addedPercent: b.share == null ? null : Math.round(b.share * 100),
|
|
@@ -20950,7 +21000,7 @@ const HARBOR_TOOLS = [
|
|
|
20950
21000
|
tool({
|
|
20951
21001
|
name: "scope_growth",
|
|
20952
21002
|
title: "Barnacles: work added since the plan was set",
|
|
20953
|
-
description: "How much a project's plan has grown since it was set (barnacles): tasks made after the baseline was set (or, without a baseline time, after the project's first week of tasks), as a count and a percent of what was planned, by week (weekOf is a Monday) and by who added them (who is null for tasks made before Harbor kept track, or by a form). daysOfWork puts them at the crew's pace (tasks finished a week over the last six); slipDays is how late the project runs against its planned end (the likely finish at that pace, or the end date's move from the baseline), and daysLine says how much of it the added work explains." + DATA_NOTE,
|
|
21003
|
+
description: "How much a project's plan has grown since it was set (barnacles): tasks made after the baseline was set (or, without a baseline time, after the project's first week of tasks), as a count and a percent of what was planned, by week (weekOf is a Monday) and by who added them (who is null for tasks made before Harbor kept track, or by a form). daysOfWork puts them at the crew's pace (tasks finished a week over the last six); slipDays is how late the project runs against its planned end (the likely finish at that pace, or the end date's move from the baseline), and daysLine says how much of it the added work explains. settling is true while a project without a baseline is still in its first week (since is in the future): nothing counts as added yet, and setting a baseline counts from now." + DATA_NOTE,
|
|
20954
21004
|
input: object({
|
|
20955
21005
|
harbor: harborArg,
|
|
20956
21006
|
project: string().min(1).max(200).describe("Project id, name or key.")
|
|
@@ -21940,104 +21990,6 @@ var init_session = __esmMin((() => {
|
|
|
21940
21990
|
init_format();
|
|
21941
21991
|
}));
|
|
21942
21992
|
|
|
21943
|
-
//#endregion
|
|
21944
|
-
//#region src/lib/sample-project.ts
|
|
21945
|
-
function sampleProject(opts) {
|
|
21946
|
-
const { ownerId, today, now, makeId } = opts;
|
|
21947
|
-
const day = (n) => todayISO(addDays(parseDate$1(today), n));
|
|
21948
|
-
const end = opts.programEnd && opts.programEnd < day(30) ? opts.programEnd : day(30);
|
|
21949
|
-
const projectId = makeId("pj");
|
|
21950
|
-
const project = {
|
|
21951
|
-
id: projectId,
|
|
21952
|
-
programId: opts.programId,
|
|
21953
|
-
name: SAMPLE_PROJECT_NAME,
|
|
21954
|
-
description: "A project to try things on. Move cards, add tasks, change dates. Delete it from Details when you’re done.",
|
|
21955
|
-
swatch: "sea",
|
|
21956
|
-
ownerId,
|
|
21957
|
-
health: "on-track",
|
|
21958
|
-
startDate: today,
|
|
21959
|
-
endDate: end,
|
|
21960
|
-
budget: 0,
|
|
21961
|
-
spent: 0
|
|
21962
|
-
};
|
|
21963
|
-
const task = (title, status, o = {}) => ({
|
|
21964
|
-
id: makeId("t"),
|
|
21965
|
-
projectId,
|
|
21966
|
-
title,
|
|
21967
|
-
description: o.description ?? "",
|
|
21968
|
-
status,
|
|
21969
|
-
priority: o.priority ?? "medium",
|
|
21970
|
-
assigneeId: ownerId,
|
|
21971
|
-
dueDate: o.due == null ? null : day(o.due),
|
|
21972
|
-
createdAt: now,
|
|
21973
|
-
completedAt: status === "done" ? now : null,
|
|
21974
|
-
checklist: (o.checklist ?? []).map(([t, done]) => ({
|
|
21975
|
-
id: makeId("ck"),
|
|
21976
|
-
title: t,
|
|
21977
|
-
done
|
|
21978
|
-
})),
|
|
21979
|
-
blockedById: o.waitsOn ?? null,
|
|
21980
|
-
blockedByIds: o.waitsOn ? [o.waitsOn] : []
|
|
21981
|
-
});
|
|
21982
|
-
const draft = task("Draft the new home page", "doing", {
|
|
21983
|
-
description: "A sample card. Tick the checklist, leave a comment, or drag it to Review on the board.",
|
|
21984
|
-
due: 3,
|
|
21985
|
-
priority: "high",
|
|
21986
|
-
checklist: [
|
|
21987
|
-
["Headline and one line under it", true],
|
|
21988
|
-
["Three reasons to stay", false],
|
|
21989
|
-
["One clear button", false]
|
|
21990
|
-
]
|
|
21991
|
-
});
|
|
21992
|
-
return {
|
|
21993
|
-
project,
|
|
21994
|
-
tasks: [
|
|
21995
|
-
task("Agree what the refresh is for", "done", { description: "Finished. Move it back if it turns out it wasn’t." }),
|
|
21996
|
-
task("List the pages people actually use", "done"),
|
|
21997
|
-
draft,
|
|
21998
|
-
task("Write the pricing page copy", "review", {
|
|
21999
|
-
description: "Waiting for a second pair of eyes. Move it to Done when it reads well.",
|
|
22000
|
-
due: 5
|
|
22001
|
-
}),
|
|
22002
|
-
task("Pick the photos", "todo", {
|
|
22003
|
-
description: "Hand this one to someone else from the card’s Owner field.",
|
|
22004
|
-
due: 7
|
|
22005
|
-
}),
|
|
22006
|
-
task("Check every page on a phone", "todo", {
|
|
22007
|
-
description: "Waits on the home page draft, so it shows as waiting until that card is done.",
|
|
22008
|
-
due: 10,
|
|
22009
|
-
waitsOn: draft.id
|
|
22010
|
-
}),
|
|
22011
|
-
task("Send old links to the new pages", "backlog", { description: "No date yet. Backlog is for what isn’t planned." }),
|
|
22012
|
-
task("Tell customers what changed", "backlog", { priority: "low" })
|
|
22013
|
-
],
|
|
22014
|
-
raidItems: [{
|
|
22015
|
-
id: makeId("r"),
|
|
22016
|
-
kind: "risk",
|
|
22017
|
-
projectId,
|
|
22018
|
-
title: "Photos may not be cleared in time",
|
|
22019
|
-
description: "A sample risk. If the photos aren’t licensed before launch, the home page ships with placeholders.",
|
|
22020
|
-
severity: "medium",
|
|
22021
|
-
status: "open",
|
|
22022
|
-
ownerId,
|
|
22023
|
-
dueDate: day(7),
|
|
22024
|
-
probability: 3,
|
|
22025
|
-
impact: 3,
|
|
22026
|
-
mitigation: "Shortlist stock photos we already own as a fallback.",
|
|
22027
|
-
createdById: ownerId,
|
|
22028
|
-
createdAt: now,
|
|
22029
|
-
updatedAt: now,
|
|
22030
|
-
updates: []
|
|
22031
|
-
}]
|
|
22032
|
-
};
|
|
22033
|
-
}
|
|
22034
|
-
var SAMPLE_PROJECT_NAME;
|
|
22035
|
-
var init_sample_project = __esmMin((() => {
|
|
22036
|
-
init_date_fns();
|
|
22037
|
-
init_format();
|
|
22038
|
-
SAMPLE_PROJECT_NAME = "Website refresh (sample)";
|
|
22039
|
-
}));
|
|
22040
|
-
|
|
22041
21993
|
//#endregion
|
|
22042
21994
|
//#region src/lib/first-week.ts
|
|
22043
21995
|
/** The program launch named for you, when nobody has renamed it yet. */
|
|
@@ -22106,7 +22058,7 @@ function hierarchySlice({ get, write }) {
|
|
|
22106
22058
|
get().log(`opened project ${row.name}`, `/projects/${id}`);
|
|
22107
22059
|
return id;
|
|
22108
22060
|
},
|
|
22109
|
-
addSampleProject: () => {
|
|
22061
|
+
addSampleProject: (sampleProject) => {
|
|
22110
22062
|
const state = get();
|
|
22111
22063
|
const program = state.programs[0];
|
|
22112
22064
|
if (!program) return null;
|
|
@@ -22481,7 +22433,6 @@ var init_hierarchy = __esmMin((() => {
|
|
|
22481
22433
|
init_shared();
|
|
22482
22434
|
init_hill();
|
|
22483
22435
|
init_charter_lines();
|
|
22484
|
-
init_sample_project();
|
|
22485
22436
|
init_format();
|
|
22486
22437
|
init_empty();
|
|
22487
22438
|
init_first_week();
|
|
@@ -26125,7 +26076,7 @@ function changesFrom(data, project, flags, now) {
|
|
|
26125
26076
|
});
|
|
26126
26077
|
return changes;
|
|
26127
26078
|
}
|
|
26128
|
-
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";
|
|
26129
26080
|
const very = (t) => t.kind === "forecast" && t.veryLikely ? shortDate(t.veryLikely) : "-";
|
|
26130
26081
|
/** What `harbor try` prints: the changes, then the two plans side by side. */
|
|
26131
26082
|
function tryLines(data, project, changes, cmp, today) {
|
package/lib/cli.mjs
CHANGED
|
@@ -205,7 +205,7 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
205
205
|
the review: programs worst first,
|
|
206
206
|
each with its next move (reads
|
|
207
207
|
only; harbor submit moves a task)
|
|
208
|
-
harbor thanks [--all] [--given] thanks
|
|
208
|
+
harbor thanks [--all] [--given] your thanks, last 30 days
|
|
209
209
|
harbor thanks TASK "LINE" [--to NAME] a one-line thanks for a task
|
|
210
210
|
harbor thanks --milestone NAME|--decision TITLE "LINE" [--to NAME]
|
|
211
211
|
the same for a milestone or a
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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 ??
|
|
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(
|
|
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
|
-
|
|
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/lib/format.mjs
CHANGED
|
@@ -643,6 +643,13 @@ export function whyText(r) {
|
|
|
643
643
|
/** A project's barnacles, as scope_growth answers it: the growth, its cost in days, by week and by who. */
|
|
644
644
|
export function barnaclesText(r) {
|
|
645
645
|
const head = [`Barnacles · ${r.project}`];
|
|
646
|
+
if (r.settling) {
|
|
647
|
+
head.push(
|
|
648
|
+
`The plan is still being written: ${r.planned} task${r.planned === 1 ? "" : "s"} so far, and nothing counts as added until ${monthDay(r.since)}, a week after its first task.`,
|
|
649
|
+
`harbor baseline "${r.key ?? r.project}" --set counts from now instead.`,
|
|
650
|
+
);
|
|
651
|
+
return head.join("\n");
|
|
652
|
+
}
|
|
646
653
|
if (!r.added) {
|
|
647
654
|
head.push(`Nothing added since the plan was set (${r.since.slice(0, 10)}): ${r.planned} task${r.planned === 1 ? "" : "s"}, as planned.`);
|
|
648
655
|
return head.join("\n");
|
|
@@ -1198,7 +1205,7 @@ export function catchupText(r) {
|
|
|
1198
1205
|
m.added ? `${m.added} added` : null,
|
|
1199
1206
|
m.health ? `now ${HEALTH[m.health.to] ?? m.health.to} (was ${HEALTH[m.health.from] ?? m.health.from})` : null,
|
|
1200
1207
|
].filter(Boolean);
|
|
1201
|
-
out.push(` ${m.project}: ${bits.join(", ")}`);
|
|
1208
|
+
out.push(` ${m.key ? `${m.key} ` : ""}${m.project}: ${bits.join(", ")}`);
|
|
1202
1209
|
}
|
|
1203
1210
|
}
|
|
1204
1211
|
const more = r.handed.length > r.lookFirst.filter((l) => l.kind === "handed").length;
|