@harborppm/cli 0.7.0 → 0.8.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 +5 -5
- package/dist/app.mjs +97 -58
- package/lib/auth.mjs +6 -3
- package/lib/cli.mjs +54 -20
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Harbor CLI
|
|
2
2
|
|
|
3
|
-
[Harbor](https://harborppm.com) in the terminal: a full-screen app with your whole harbor, and commands for scripts and agents.
|
|
3
|
+
[Harbor](https://harborppm.com/?utm_source=npm&utm_medium=readme&utm_campaign=cli) in the terminal: a full-screen app with your whole harbor, and commands for scripts and agents.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm install -g @harborppm/cli
|
|
@@ -68,7 +68,7 @@ harbor mine what's on you, late first
|
|
|
68
68
|
harbor tasks [words] [--project P] [--assignee me|none|NAME] [--late] [--status S,S]
|
|
69
69
|
harbor show TASK one task in full
|
|
70
70
|
|
|
71
|
-
harbor add "TITLE" --project P [--assignee me|NAME] [--due
|
|
71
|
+
harbor add "TITLE" --project P [--assignee me|NAME] [--due DATE]
|
|
72
72
|
[--priority low|medium|high|urgent] [--status S] [--over-limit]
|
|
73
73
|
harbor start TASK | review TASK | done TASK [--confirm] [--over-limit]
|
|
74
74
|
harbor edit TASK [--title T] [--assignee me|none|NAME] [--due DATE|none]
|
|
@@ -76,12 +76,12 @@ harbor edit TASK [--title T] [--assignee me|none|NAME] [--due DATE|none]
|
|
|
76
76
|
harbor comment TASK "TEXT"
|
|
77
77
|
harbor checkin "WHAT YOU'RE DOING" [--on-blocker]
|
|
78
78
|
harbor risk "TITLE" --project P|--program P [--impact 1-5] [--probability 1-5]
|
|
79
|
-
[--owner NAME] [--due
|
|
79
|
+
[--owner NAME] [--due DATE]
|
|
80
80
|
harbor issue "TITLE" --project P|--program P [--impact 1-5] [--owner NAME]
|
|
81
|
-
[--due
|
|
81
|
+
[--due DATE]
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
Every command takes `--json` for the raw result, and `harbor COMMAND --help` shows just that command. Statuses are backlog, todo, doing, review and done.
|
|
84
|
+
Every command takes `--json` for the raw result, 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.
|
|
85
85
|
|
|
86
86
|
When you belong to more than one harbor, say which: `--harbor NAME` on the command, or `harbor use NAME` once (it's saved with your sign-in; `harbor use none` forgets it). With neither, the commands ask rather than guess, so nothing lands in the wrong harbor. The full-screen app opens the one `harbor use` set too.
|
|
87
87
|
|
package/dist/app.mjs
CHANGED
|
@@ -2298,12 +2298,21 @@ function PriorityMark({ priority }) {
|
|
|
2298
2298
|
function Heading({ children, right }) {
|
|
2299
2299
|
return /* @__PURE__ */ jsxs(Box, {
|
|
2300
2300
|
justifyContent: "space-between",
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2301
|
+
columnGap: 2,
|
|
2302
|
+
children: [/* @__PURE__ */ jsx(Box, {
|
|
2303
|
+
flexShrink: 1,
|
|
2304
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
2305
|
+
bold: true,
|
|
2306
|
+
wrap: "truncate-end",
|
|
2307
|
+
children
|
|
2308
|
+
})
|
|
2309
|
+
}), right ? /* @__PURE__ */ jsx(Box, {
|
|
2310
|
+
flexShrink: 2,
|
|
2311
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
2312
|
+
color: theme.muted,
|
|
2313
|
+
wrap: "truncate-end",
|
|
2314
|
+
children: right
|
|
2315
|
+
})
|
|
2307
2316
|
}) : null]
|
|
2308
2317
|
});
|
|
2309
2318
|
}
|
|
@@ -2917,7 +2926,7 @@ function HistoryView({ active, width, height, go }) {
|
|
|
2917
2926
|
on ? "› " : " ",
|
|
2918
2927
|
/* @__PURE__ */ jsx(Text, {
|
|
2919
2928
|
color: theme.muted,
|
|
2920
|
-
children: timeAgo(parseIso(row.at))
|
|
2929
|
+
children: `${timeAgo(parseIso(row.at))} `.padEnd(16)
|
|
2921
2930
|
}),
|
|
2922
2931
|
/* @__PURE__ */ jsx(Text, {
|
|
2923
2932
|
bold: true,
|
|
@@ -2972,7 +2981,9 @@ function constraintIdle(data, constraint) {
|
|
|
2972
2981
|
if (constraint.status !== "active" || !constraint.ownerId) return false;
|
|
2973
2982
|
const tasks = tasksInProgramOf(data, constraint.programId);
|
|
2974
2983
|
if (!tasks.some((t) => t.status !== "done")) return false;
|
|
2975
|
-
|
|
2984
|
+
if (tasks.some((t) => t.assigneeId === constraint.ownerId && t.status === "doing")) return false;
|
|
2985
|
+
const here = new Set(tasks.map((t) => t.id));
|
|
2986
|
+
return data.tasks.some((t) => t.assigneeId === constraint.ownerId && t.status === "doing" && !here.has(t.id));
|
|
2976
2987
|
}
|
|
2977
2988
|
|
|
2978
2989
|
//#endregion
|
|
@@ -3267,6 +3278,7 @@ function weeklyLoad(tasks, personId, now, weeks = 6) {
|
|
|
3267
3278
|
|
|
3268
3279
|
//#endregion
|
|
3269
3280
|
//#region src/tui/views/lists.tsx
|
|
3281
|
+
const count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
3270
3282
|
function RowList({ title, right, rows, empty, active, width, height, onOpen, keys = {} }) {
|
|
3271
3283
|
const [at, setAt] = useState(0);
|
|
3272
3284
|
const row = rows[Math.min(at, rows.length - 1)];
|
|
@@ -3368,7 +3380,7 @@ function ProjectsView({ active, width, height, go }) {
|
|
|
3368
3380
|
]);
|
|
3369
3381
|
return /* @__PURE__ */ jsx(RowList, {
|
|
3370
3382
|
title: "Portfolios, programs and projects",
|
|
3371
|
-
right: `${projects.length}
|
|
3383
|
+
right: `${count(projects.length, "project")} · n new project · e edit`,
|
|
3372
3384
|
rows,
|
|
3373
3385
|
empty: "No projects yet. n adds one.",
|
|
3374
3386
|
active,
|
|
@@ -3413,7 +3425,7 @@ function ProgramsView({ active, width, height, go }) {
|
|
|
3413
3425
|
]);
|
|
3414
3426
|
return /* @__PURE__ */ jsx(RowList, {
|
|
3415
3427
|
title: "Programs",
|
|
3416
|
-
right: `${programs.length}
|
|
3428
|
+
right: `${count(programs.length, "program")} · n new · e edit`,
|
|
3417
3429
|
rows,
|
|
3418
3430
|
empty: "No programs yet. n adds one.",
|
|
3419
3431
|
active,
|
|
@@ -3502,7 +3514,7 @@ function PeopleView({ active, width, height, go }) {
|
|
|
3502
3514
|
]);
|
|
3503
3515
|
return /* @__PURE__ */ jsx(RowList, {
|
|
3504
3516
|
title: "People",
|
|
3505
|
-
right: `${people.length} people · Enter to change`,
|
|
3517
|
+
right: `${people.length === 1 ? "1 person" : `${people.length} people`} · Enter to change`,
|
|
3506
3518
|
rows,
|
|
3507
3519
|
empty: "Nobody here yet. Invite people from the website.",
|
|
3508
3520
|
active,
|
|
@@ -3801,7 +3813,7 @@ function NewTask({ projectId, status, width, onDone, onFull }) {
|
|
|
3801
3813
|
if (!chosen) return /* @__PURE__ */ jsx(Box, {
|
|
3802
3814
|
width,
|
|
3803
3815
|
children: /* @__PURE__ */ jsx(Picker, {
|
|
3804
|
-
title: "New task in which project?",
|
|
3816
|
+
title: projects.length ? "New task in which project?" : "No projects yet. Esc, then 3 for Projects and n to start one.",
|
|
3805
3817
|
choices: projects.map((p) => ({
|
|
3806
3818
|
label: p.name,
|
|
3807
3819
|
value: p.id,
|
|
@@ -3816,7 +3828,7 @@ function NewTask({ projectId, status, width, onDone, onFull }) {
|
|
|
3816
3828
|
children: /* @__PURE__ */ jsx(TextField, {
|
|
3817
3829
|
label: `New task in ${chosen.name}${status ? ` · ${STATUS_LABEL[status]}` : ""}`,
|
|
3818
3830
|
placeholder: "What needs doing?",
|
|
3819
|
-
hint:
|
|
3831
|
+
hint: `Enter adds it (yours, ${STATUS_LABEL[status ?? "todo"].toLowerCase()}) · Esc leaves it`,
|
|
3820
3832
|
onCancel: () => onDone(null),
|
|
3821
3833
|
onSubmit: (title) => {
|
|
3822
3834
|
if (!title.trim()) return onDone(null);
|
|
@@ -4911,7 +4923,7 @@ function PortfolioView({ portfolioId, active, width, height, go, onBack }) {
|
|
|
4911
4923
|
}),
|
|
4912
4924
|
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
4913
4925
|
/* @__PURE__ */ jsx(Heading, {
|
|
4914
|
-
right: `${list.length}
|
|
4926
|
+
right: `${list.length} program${list.length === 1 ? "" : "s"}`,
|
|
4915
4927
|
children: "Programs"
|
|
4916
4928
|
}),
|
|
4917
4929
|
list.length === 0 ? /* @__PURE__ */ jsx(Text, {
|
|
@@ -5047,6 +5059,9 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
|
|
|
5047
5059
|
const { done, total } = projectSummary(data, project);
|
|
5048
5060
|
const lead = people.find((p) => p.id === project.ownerId);
|
|
5049
5061
|
const body = height - 3;
|
|
5062
|
+
const fit = Math.max(1, Math.floor((width - 2) / 14));
|
|
5063
|
+
const firstColumn = Math.min(Math.max(0, col - Math.floor(fit / 2)), Math.max(0, columns.length - fit));
|
|
5064
|
+
const shownColumns = columns.slice(firstColumn, firstColumn + fit);
|
|
5050
5065
|
return /* @__PURE__ */ jsxs(Box, {
|
|
5051
5066
|
flexDirection: "column",
|
|
5052
5067
|
width,
|
|
@@ -5055,36 +5070,44 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
|
|
|
5055
5070
|
children: [
|
|
5056
5071
|
/* @__PURE__ */ jsxs(Box, {
|
|
5057
5072
|
justifyContent: "space-between",
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5073
|
+
columnGap: 2,
|
|
5074
|
+
children: [/* @__PURE__ */ jsx(Box, {
|
|
5075
|
+
flexShrink: 1,
|
|
5076
|
+
children: /* @__PURE__ */ jsxs(Text, {
|
|
5077
|
+
wrap: "truncate-end",
|
|
5078
|
+
children: [
|
|
5079
|
+
/* @__PURE__ */ jsx(Text, {
|
|
5080
|
+
bold: true,
|
|
5081
|
+
children: project.name
|
|
5082
|
+
}),
|
|
5083
|
+
" ",
|
|
5084
|
+
/* @__PURE__ */ jsx(Text, {
|
|
5085
|
+
color: theme.muted,
|
|
5086
|
+
children: project.key
|
|
5087
|
+
}),
|
|
5088
|
+
" ",
|
|
5089
|
+
/* @__PURE__ */ jsx(HealthMark, {
|
|
5090
|
+
health: project.health,
|
|
5091
|
+
label: true
|
|
5092
|
+
})
|
|
5093
|
+
]
|
|
5094
|
+
})
|
|
5095
|
+
}), /* @__PURE__ */ jsx(Box, {
|
|
5096
|
+
flexShrink: 2,
|
|
5097
|
+
children: /* @__PURE__ */ jsxs(Text, {
|
|
5098
|
+
color: theme.muted,
|
|
5099
|
+
wrap: "truncate-end",
|
|
5100
|
+
children: [
|
|
5101
|
+
done,
|
|
5102
|
+
" of ",
|
|
5103
|
+
total,
|
|
5104
|
+
" done",
|
|
5105
|
+
lead ? ` · ${lead.name}` : "",
|
|
5106
|
+
" · ",
|
|
5107
|
+
mode,
|
|
5108
|
+
" · v next · e details"
|
|
5109
|
+
]
|
|
5110
|
+
})
|
|
5088
5111
|
})]
|
|
5089
5112
|
}),
|
|
5090
5113
|
/* @__PURE__ */ jsx(Text, {
|
|
@@ -5100,8 +5123,9 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
|
|
|
5100
5123
|
today: day
|
|
5101
5124
|
}) : mode === "board" ? /* @__PURE__ */ jsx(Box, {
|
|
5102
5125
|
height: body,
|
|
5103
|
-
children:
|
|
5104
|
-
const
|
|
5126
|
+
children: shownColumns.map((c, i) => {
|
|
5127
|
+
const ci = firstColumn + i;
|
|
5128
|
+
const w = Math.floor((width - 2) / shownColumns.length);
|
|
5105
5129
|
const on = active && ci === col;
|
|
5106
5130
|
const r = Math.min(rowByCol[ci] ?? 0, Math.max(0, c.tasks.length - 1));
|
|
5107
5131
|
const view = windowOf(c.tasks, r, Math.max(1, Math.floor((body - 3) / 3)));
|
|
@@ -5113,19 +5137,33 @@ function ProjectView({ projectId, active, width, height, go, onBack }) {
|
|
|
5113
5137
|
borderColor: on ? theme.accent : theme.border,
|
|
5114
5138
|
paddingX: 1,
|
|
5115
5139
|
children: [
|
|
5116
|
-
/* @__PURE__ */ jsxs(
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5140
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
5141
|
+
i === 0 && firstColumn > 0 ? /* @__PURE__ */ jsx(Text, {
|
|
5142
|
+
color: theme.muted,
|
|
5143
|
+
children: "‹ "
|
|
5144
|
+
}) : null,
|
|
5145
|
+
/* @__PURE__ */ jsx(Box, {
|
|
5146
|
+
flexGrow: 1,
|
|
5147
|
+
flexShrink: 1,
|
|
5148
|
+
children: /* @__PURE__ */ jsxs(Text, {
|
|
5149
|
+
bold: true,
|
|
5150
|
+
color: on ? theme.accent : void 0,
|
|
5151
|
+
wrap: "truncate-end",
|
|
5152
|
+
children: [
|
|
5153
|
+
c.label.toUpperCase(),
|
|
5154
|
+
" ",
|
|
5155
|
+
/* @__PURE__ */ jsxs(Text, {
|
|
5156
|
+
color: over ? theme.bad : theme.muted,
|
|
5157
|
+
children: [c.tasks.length, c.limit != null ? `/${c.limit}` : ""]
|
|
5158
|
+
})
|
|
5159
|
+
]
|
|
5126
5160
|
})
|
|
5127
|
-
|
|
5128
|
-
|
|
5161
|
+
}),
|
|
5162
|
+
i === shownColumns.length - 1 && ci < columns.length - 1 ? /* @__PURE__ */ jsx(Text, {
|
|
5163
|
+
color: theme.muted,
|
|
5164
|
+
children: " ›"
|
|
5165
|
+
}) : null
|
|
5166
|
+
] }),
|
|
5129
5167
|
c.tasks.length === 0 ? /* @__PURE__ */ jsx(Text, {
|
|
5130
5168
|
color: theme.muted,
|
|
5131
5169
|
children: "—"
|
|
@@ -8729,6 +8767,7 @@ function httpTransport(source, fetcher = fetch) {
|
|
|
8729
8767
|
}
|
|
8730
8768
|
} catch (error) {
|
|
8731
8769
|
if (error instanceof TransportError) throw error;
|
|
8770
|
+
if (error?.signedOut) throw error;
|
|
8732
8771
|
throw new TransportError("Can't reach Harbor.", 0, true);
|
|
8733
8772
|
}
|
|
8734
8773
|
const answer = await res.json().catch(() => ({}));
|
package/lib/auth.mjs
CHANGED
|
@@ -67,7 +67,7 @@ export async function listenForCode(state, { timeoutMs = 5 * 60_000 } = {}) {
|
|
|
67
67
|
if (ok) settle.resolve(url.searchParams.get("code"));
|
|
68
68
|
else
|
|
69
69
|
settle.reject(
|
|
70
|
-
new
|
|
70
|
+
new HarborError(
|
|
71
71
|
error === "access_denied"
|
|
72
72
|
? "You declined in the browser."
|
|
73
73
|
: "The sign-in didn't come back as expected.",
|
|
@@ -77,7 +77,7 @@ export async function listenForCode(state, { timeoutMs = 5 * 60_000 } = {}) {
|
|
|
77
77
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
78
78
|
const { port } = server.address();
|
|
79
79
|
const timer = setTimeout(
|
|
80
|
-
() => settle.reject(new
|
|
80
|
+
() => settle.reject(new HarborError("No answer from the browser in 5 minutes. Run harbor login to try again.")),
|
|
81
81
|
timeoutMs,
|
|
82
82
|
);
|
|
83
83
|
const done = code.finally(() => {
|
|
@@ -178,7 +178,10 @@ export async function refresh(creds, { fetcher = fetch, now = Date.now } = {}) {
|
|
|
178
178
|
// Refused: the sign-in was disconnected in Harbor, or renewed by another
|
|
179
179
|
// copy of these credentials (a refresh token is good once).
|
|
180
180
|
if (res.status === 400 || res.status === 401) {
|
|
181
|
-
|
|
181
|
+
const ended = new HarborError("Your sign-in on this machine has ended. Run harbor login.");
|
|
182
|
+
// The full-screen app says this rather than "Can't reach Harbor".
|
|
183
|
+
Object.assign(ended, { signedOut: true });
|
|
184
|
+
throw ended;
|
|
182
185
|
}
|
|
183
186
|
const tokens = await json(res, "Refreshing your sign-in");
|
|
184
187
|
return toCredentials(creds.server, creds.tokenEndpoint, creds.clientId, tokens, now());
|
package/lib/cli.mjs
CHANGED
|
@@ -23,36 +23,39 @@ import { jsonText, plain } from "./plain.mjs";
|
|
|
23
23
|
|
|
24
24
|
export const HELP = `harbor: Harbor from the terminal
|
|
25
25
|
|
|
26
|
-
harbor the full-screen app
|
|
27
|
-
harbor signup [--server URL] new to Harbor: make an account
|
|
26
|
+
harbor the full-screen app, by keyboard
|
|
27
|
+
harbor signup [--server URL] new to Harbor: make an account
|
|
28
28
|
harbor login [--server URL] [--read-only] sign in (opens your browser once)
|
|
29
29
|
harbor logout forget the sign-in on this machine
|
|
30
30
|
harbor harbors the harbors you belong to
|
|
31
|
-
harbor use HARBOR|none
|
|
32
|
-
harbor theme [auto|light|dark] the app's colours
|
|
31
|
+
harbor use HARBOR|none which harbor commands use
|
|
32
|
+
harbor theme [auto|light|dark] the app's colours
|
|
33
33
|
harbor --version which version this is
|
|
34
34
|
|
|
35
35
|
harbor mine what's on you, late first
|
|
36
|
-
harbor tasks [words] [--project P] [--assignee me|none|NAME]
|
|
37
|
-
|
|
36
|
+
harbor tasks [words] [--project P] [--assignee me|none|NAME]
|
|
37
|
+
[--late] [--status S,S]
|
|
38
|
+
harbor show TASK one task (TASK: a key like RR-42)
|
|
38
39
|
|
|
39
|
-
harbor add "TITLE" --project P [--assignee me|NAME] [--due
|
|
40
|
-
[--priority low|medium|high|urgent] [--status S]
|
|
40
|
+
harbor add "TITLE" --project P [--assignee me|NAME] [--due DATE]
|
|
41
|
+
[--priority low|medium|high|urgent] [--status S]
|
|
42
|
+
[--over-limit]
|
|
41
43
|
harbor start TASK | review TASK | done TASK [--confirm] [--over-limit]
|
|
42
44
|
harbor edit TASK [--title T] [--assignee me|none|NAME] [--due DATE|none]
|
|
43
45
|
[--priority P] [--status S] [--confirm] [--over-limit]
|
|
44
46
|
harbor comment TASK "TEXT"
|
|
45
47
|
harbor checkin "WHAT YOU'RE DOING" [--on-blocker]
|
|
46
48
|
harbor risk "TITLE" --project P|--program P [--impact 1-5] [--probability 1-5]
|
|
47
|
-
[--owner NAME] [--due
|
|
49
|
+
[--owner NAME] [--due DATE]
|
|
48
50
|
harbor issue "TITLE" --project P|--program P [--impact 1-5] [--owner NAME]
|
|
49
|
-
[--due
|
|
51
|
+
[--due DATE]
|
|
50
52
|
|
|
51
|
-
Every command takes --json, and --harbor NAME when you're in more than one
|
|
52
|
-
(or set one with harbor use). harbor COMMAND
|
|
53
|
+
Every command takes --json, and --harbor NAME when you're in more than one
|
|
54
|
+
harbor (or set one with harbor use). harbor help COMMAND shows just that one.
|
|
53
55
|
Statuses: backlog, todo, doing, review, done.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
DATE: YYYY-MM-DD, today, tomorrow, or +N for N days from today.
|
|
57
|
+
Changes show in Harbor's History as yours, "via Harbor CLI". These commands
|
|
58
|
+
never delete; the full-screen app can, as the website can, and asks first.
|
|
56
59
|
`;
|
|
57
60
|
|
|
58
61
|
export const VERSION = JSON.parse(
|
|
@@ -125,6 +128,16 @@ const level = (v, name) => {
|
|
|
125
128
|
};
|
|
126
129
|
const nullable = (v) => (v === "none" ? null : v);
|
|
127
130
|
|
|
131
|
+
/** --due as people say it: today, tomorrow, +3, or a date. Anything else goes to Harbor to judge. */
|
|
132
|
+
export function dueDate(v, today = new Date()) {
|
|
133
|
+
if (v === undefined || v === null) return v;
|
|
134
|
+
const words = { today: 0, tomorrow: 1 };
|
|
135
|
+
const days = v in words ? words[v] : /^\+\d{1,3}$/.test(v) ? Number(v.slice(1)) : null;
|
|
136
|
+
if (days === null) return v;
|
|
137
|
+
const d = new Date(today.getFullYear(), today.getMonth(), today.getDate() + days);
|
|
138
|
+
return todayLocal(d);
|
|
139
|
+
}
|
|
140
|
+
|
|
128
141
|
/** Harbor's tool fields, as the CLI's flags. */
|
|
129
142
|
const FLAG = {
|
|
130
143
|
due: "--due",
|
|
@@ -158,7 +171,13 @@ export function hint(message) {
|
|
|
158
171
|
const [path, ...why] = issue.split(": ");
|
|
159
172
|
const field = path.replace(/\.\d+$/, "");
|
|
160
173
|
const flag = FLAG[field] ?? `--${field}`;
|
|
161
|
-
|
|
174
|
+
// zod's "Invalid option: expected one of "low"|"high"" reads as the choices.
|
|
175
|
+
const said = why
|
|
176
|
+
.join(": ")
|
|
177
|
+
.replace(/^Invalid option: expected one of (.*)$/, (_, list) =>
|
|
178
|
+
`pick one of ${list.replace(/"/g, "").split("|").join(", ")}`,
|
|
179
|
+
);
|
|
180
|
+
if (!seen.has(flag)) seen.set(flag, said);
|
|
162
181
|
}
|
|
163
182
|
return [...seen].map(([flag, why]) => `${flag}: ${why}`).join("\n");
|
|
164
183
|
}
|
|
@@ -168,6 +187,10 @@ export function hint(message) {
|
|
|
168
187
|
"Say which with --harbor NAME, or set one with harbor use NAME.",
|
|
169
188
|
)
|
|
170
189
|
.replace(/Find it with search_tasks\.$/, "Find it with harbor tasks.")
|
|
190
|
+
.replace(
|
|
191
|
+
/^Nothing to change: pass a field to update\.$/,
|
|
192
|
+
"Nothing to change. Say what with --title, --assignee, --due, --priority or --status.",
|
|
193
|
+
)
|
|
171
194
|
.replace(
|
|
172
195
|
/^This connection was allowed to read only\..*$/s,
|
|
173
196
|
"This sign-in can only read. To make changes, run harbor login again.",
|
|
@@ -233,6 +256,13 @@ export async function run(argv, io = {}) {
|
|
|
233
256
|
const [command, ...rest] = commandFirst(argv);
|
|
234
257
|
|
|
235
258
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
259
|
+
// `harbor help mine`: that command's help, as `harbor mine --help` gives it.
|
|
260
|
+
if (rest[0]) {
|
|
261
|
+
const help = commandHelp(rest[0]);
|
|
262
|
+
if (help === null) throw new HarborError(`No command "${rest[0]}". Run harbor help.`);
|
|
263
|
+
out(help);
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
236
266
|
const top = banner(io, env, ({ bold, dim }) => [
|
|
237
267
|
bold("Harbor"),
|
|
238
268
|
"Portfolios, programs and projects,",
|
|
@@ -382,6 +412,10 @@ export async function run(argv, io = {}) {
|
|
|
382
412
|
return 0;
|
|
383
413
|
}
|
|
384
414
|
|
|
415
|
+
// A mistyped command says so, signed in or not.
|
|
416
|
+
if (commandHelp(command) === null) {
|
|
417
|
+
throw new HarborError(`No command "${command}". Run harbor help.`);
|
|
418
|
+
}
|
|
385
419
|
const creds = await readCredentials(env);
|
|
386
420
|
if (!creds)
|
|
387
421
|
throw new HarborError("Not signed in. Run harbor login, or harbor signup if you're new.");
|
|
@@ -419,7 +453,7 @@ export async function run(argv, io = {}) {
|
|
|
419
453
|
out(
|
|
420
454
|
creds.harbor
|
|
421
455
|
? plain(`Commands use ${creds.harborName ?? creds.harbor}.`)
|
|
422
|
-
: "No harbor set
|
|
456
|
+
: "No harbor set. In one harbor, commands use it; in several, set one with harbor use NAME or say --harbor NAME.",
|
|
423
457
|
);
|
|
424
458
|
return 0;
|
|
425
459
|
}
|
|
@@ -518,7 +552,7 @@ export async function run(argv, io = {}) {
|
|
|
518
552
|
project: values.project,
|
|
519
553
|
title: text(positionals, "the task"),
|
|
520
554
|
assignee: values.assignee,
|
|
521
|
-
due: values.due,
|
|
555
|
+
due: dueDate(values.due),
|
|
522
556
|
priority: values.priority,
|
|
523
557
|
status: values.status,
|
|
524
558
|
overLimit: values["over-limit"] || undefined,
|
|
@@ -560,7 +594,7 @@ export async function run(argv, io = {}) {
|
|
|
560
594
|
task: one(positionals, "task id"),
|
|
561
595
|
title: values.title,
|
|
562
596
|
assignee: values.assignee,
|
|
563
|
-
due: values.due === undefined ? undefined : nullable(values.due),
|
|
597
|
+
due: values.due === undefined ? undefined : nullable(dueDate(values.due)),
|
|
564
598
|
priority: values.priority,
|
|
565
599
|
status: values.status,
|
|
566
600
|
confirmed: values.confirm || undefined,
|
|
@@ -616,9 +650,9 @@ export async function run(argv, io = {}) {
|
|
|
616
650
|
owner: values.owner,
|
|
617
651
|
impact: level(values.impact, "impact"),
|
|
618
652
|
probability: level(values.probability, "probability"),
|
|
619
|
-
due: values.due,
|
|
653
|
+
due: dueDate(values.due),
|
|
620
654
|
},
|
|
621
|
-
(r) => `Logged the ${command}
|
|
655
|
+
(r) => `Logged the ${command} “${r.raised.title}”.`,
|
|
622
656
|
);
|
|
623
657
|
}
|
|
624
658
|
default:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harborppm/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"test": "node --test test/*.test.mjs",
|
|
33
33
|
"build": "node build.mjs"
|
|
34
34
|
},
|
|
35
|
-
"homepage": "https://harborppm.com/ai",
|
|
35
|
+
"homepage": "https://harborppm.com/ai?utm_source=npm&utm_medium=package&utm_campaign=cli",
|
|
36
36
|
"license": "MIT",
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"ink": "^7.1.1",
|