@navbook/cli 0.4.0 → 0.5.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/dist/args.d.ts +28 -0
- package/dist/args.js +48 -0
- package/dist/args.js.map +1 -0
- package/dist/commands/complete.d.ts +10 -0
- package/dist/commands/complete.js +59 -51
- package/dist/commands/complete.js.map +1 -1
- package/dist/commands/compose.d.ts +35 -0
- package/dist/commands/doctor.d.ts +15 -0
- package/dist/commands/entity.d.ts +73 -0
- package/dist/commands/entity.js +59 -44
- package/dist/commands/entity.js.map +1 -1
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/install.d.ts +17 -0
- package/dist/commands/install.js +6 -3
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/issue.d.ts +35 -0
- package/dist/commands/issue.js +4 -5
- package/dist/commands/issue.js.map +1 -1
- package/dist/commands/plugin.d.ts +36 -0
- package/dist/commands/plugin.js +386 -0
- package/dist/commands/plugin.js.map +1 -0
- package/dist/commands/policy.d.ts +58 -0
- package/dist/commands/policy.js +13 -0
- package/dist/commands/policy.js.map +1 -1
- package/dist/commands/pr-elsewhere.d.ts +40 -0
- package/dist/commands/pr-elsewhere.js +209 -0
- package/dist/commands/pr-elsewhere.js.map +1 -0
- package/dist/commands/pr.d.ts +69 -0
- package/dist/commands/pr.js +86 -60
- package/dist/commands/pr.js.map +1 -1
- package/dist/context.d.ts +42 -0
- package/dist/context.js +2 -0
- package/dist/context.js.map +1 -1
- package/dist/editor.d.ts +23 -0
- package/dist/editor.js +4 -0
- package/dist/editor.js.map +1 -1
- package/dist/errors.d.ts +17 -0
- package/dist/install/completions.d.ts +19 -0
- package/dist/install/hook.d.ts +32 -0
- package/dist/install/hook.js +48 -4
- package/dist/install/hook.js.map +1 -1
- package/dist/main.d.ts +21 -0
- package/dist/main.js +112 -8
- package/dist/main.js.map +1 -1
- package/dist/plugins/commands.d.ts +69 -0
- package/dist/plugins/commands.js +148 -0
- package/dist/plugins/commands.js.map +1 -0
- package/dist/plugins/hint.d.ts +23 -0
- package/dist/plugins/hint.js +65 -0
- package/dist/plugins/hint.js.map +1 -0
- package/dist/plugins/host.d.ts +125 -0
- package/dist/plugins/host.js +18 -0
- package/dist/plugins/host.js.map +1 -0
- package/dist/plugins/loader.d.ts +54 -0
- package/dist/plugins/loader.js +165 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/resolve.d.ts +50 -0
- package/dist/plugins/resolve.js +134 -0
- package/dist/plugins/resolve.js.map +1 -0
- package/dist/plugins/runtime.d.ts +69 -0
- package/dist/plugins/runtime.js +256 -0
- package/dist/plugins/runtime.js.map +1 -0
- package/dist/plugins/store.d.ts +96 -0
- package/dist/plugins/store.js +186 -0
- package/dist/plugins/store.js.map +1 -0
- package/dist/program.d.ts +49 -0
- package/dist/program.js +351 -113
- package/dist/program.js.map +1 -1
- package/dist/prompt.d.ts +31 -0
- package/dist/render/colors.d.ts +13 -0
- package/dist/render/detail.d.ts +22 -0
- package/dist/render/table.d.ts +32 -0
- package/dist/render/table.js +123 -13
- package/dist/render/table.js.map +1 -1
- package/dist/sort.d.ts +27 -0
- package/package.json +9 -2
- package/dist/commands/feature.js +0 -202
- package/dist/commands/feature.js.map +0 -1
package/dist/program.js
CHANGED
|
@@ -4,16 +4,20 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { MERGE_METHODS } from "@navbook/core";
|
|
7
|
+
import { DEADLINE_TERMS, entityJson, MERGE_METHODS, QUERY_STATUSES, queryTermsFor, REVIEW_DECISIONS, readMarkerVersion, } from "@navbook/core";
|
|
8
8
|
import { Command, Option } from "commander";
|
|
9
|
+
import { finiteNumber, wholeNumber } from "./args.js";
|
|
9
10
|
import { cmdComplete } from "./commands/complete.js";
|
|
10
11
|
import { cmdDoctor } from "./commands/doctor.js";
|
|
11
12
|
import { cmdClose, cmdComment, cmdDelete, cmdEdit, cmdList, cmdReopen, cmdShow, } from "./commands/entity.js";
|
|
12
|
-
import { cmdFeatureEdit, cmdFeatureList, cmdFeatureOpen, cmdFeatureShow, cmdSpecAdd, cmdSpecEdit, cmdSpecList, } from "./commands/feature.js";
|
|
13
13
|
import { cmdId, cmdInit } from "./commands/init.js";
|
|
14
14
|
import { cmdInstall, cmdUninstall } from "./commands/install.js";
|
|
15
15
|
import { cmdIssueLink, cmdIssueOpen, cmdIssueUnlink } from "./commands/issue.js";
|
|
16
|
+
import { cmdPluginInstall, cmdPluginList, cmdPluginRemove, cmdPluginUpdate, } from "./commands/plugin.js";
|
|
17
|
+
import { warnNewerFormat } from "./commands/policy.js";
|
|
16
18
|
import { cmdPrClose, cmdPrList, cmdPrMerge, cmdPrOpen, cmdPrRequest, cmdPrReview, cmdPrUpdate, } from "./commands/pr.js";
|
|
19
|
+
import { YES_HELP } from "./commands/pr-elsewhere.js";
|
|
20
|
+
import { applyOption, buildPluginCommand as buildDeclaredCommand, optionCollision, } from "./plugins/commands.js";
|
|
17
21
|
import { DEFAULT_SORT, SORT_ORDERS } from "./sort.js";
|
|
18
22
|
/**
|
|
19
23
|
* Read straight from package.json rather than duplicating the version as a
|
|
@@ -28,27 +32,91 @@ function readOwnVersion() {
|
|
|
28
32
|
return pkg.version;
|
|
29
33
|
}
|
|
30
34
|
export const VERSION = readOwnVersion();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
/**
|
|
36
|
+
* What each keyed term means, in the words `--help` uses.
|
|
37
|
+
*
|
|
38
|
+
* Only the prose lives here: which terms exist, which noun has them and how
|
|
39
|
+
* they combine all come from `QUERY_TERMS`, which the parser reads too, and so
|
|
40
|
+
* do the values of the three keys that take a fixed set. Keyed by `QueryKey`,
|
|
41
|
+
* so a term added to the grammar does not type-check until it is described.
|
|
42
|
+
* `status` is the one whose values differ by noun, so `queryHelp` writes it.
|
|
43
|
+
*/
|
|
44
|
+
const TERM_HELP = {
|
|
45
|
+
label: { syntax: "label:L", hint: ["L is among the entity's labels (repeatable, ANDs)"] },
|
|
46
|
+
assignee: { syntax: "assignee:EMAIL", hint: ["assignee address, or a fragment of its domain"] },
|
|
47
|
+
author: { syntax: "author:EMAIL", hint: ["author address, same matching"] },
|
|
48
|
+
milestone: { syntax: "milestone:M", hint: ["exact milestone"] },
|
|
49
|
+
reviewer: { syntax: "reviewer:EMAIL", hint: ["asked to review it; same matching"] },
|
|
50
|
+
review: { syntax: "review:DECISION", hint: [`one of ${REVIEW_DECISIONS.join(", ")}`] },
|
|
51
|
+
awaiting: { syntax: "awaiting:EMAIL", hint: ["asked to review it and has not yet"] },
|
|
52
|
+
deadline: {
|
|
53
|
+
syntax: `deadline:${DEADLINE_TERMS.join("|")}`,
|
|
54
|
+
hint: ["overdue: due before today (UTC), strictly;", "none: no deadline at all"],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
/** How wide the syntax column is, as the help has always aligned it. */
|
|
58
|
+
const SYNTAX_WIDTH = 28;
|
|
59
|
+
function helpRow(syntax, hint) {
|
|
60
|
+
return hint.map((line, i) => ` ${(i === 0 ? syntax : "").padEnd(SYNTAX_WIDTH)}${line}`);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A plugin's declared `help` line as rows of the table above.
|
|
64
|
+
*
|
|
65
|
+
* The manifest writes it as the built-in rows read, syntax then a gap then
|
|
66
|
+
* the hint; it is re-aligned here so a plugin need not know the column this
|
|
67
|
+
* help happens to use. A line with no gap is all syntax, and a syntax too wide
|
|
68
|
+
* for its column puts the hint on the next line rather than against it.
|
|
69
|
+
*/
|
|
70
|
+
export function pluginHelpRow(help) {
|
|
71
|
+
const [syntax = "", hint = ""] = help.trim().split(/\s{2,}(.*)/s);
|
|
72
|
+
if (hint === "")
|
|
73
|
+
return [` ${syntax}`];
|
|
74
|
+
if (syntax.length >= SYNTAX_WIDTH)
|
|
75
|
+
return [` ${syntax}`, ...helpRow("", [hint])];
|
|
76
|
+
return helpRow(syntax, [hint]);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The query grammar as `nav {issue,pr} list --help` states it.
|
|
80
|
+
*
|
|
81
|
+
* Built per noun, so neither page offers a term its own parser refuses. The
|
|
82
|
+
* terms plugins declared for the noun follow the built-in ones, read from
|
|
83
|
+
* their manifests alone (spec 04 §4.3: help costs no plugin code).
|
|
84
|
+
*/
|
|
85
|
+
function queryHelp(kind, pluginKeys = []) {
|
|
86
|
+
const terms = queryTermsFor(kind);
|
|
87
|
+
const rows = terms.flatMap(({ key }) => {
|
|
88
|
+
if (key === "status") {
|
|
89
|
+
return helpRow(`status:${QUERY_STATUSES[kind].join("|")}`, ["entity status (path)"]);
|
|
90
|
+
}
|
|
91
|
+
return helpRow(TERM_HELP[key].syntax, TERM_HELP[key].hint);
|
|
92
|
+
});
|
|
93
|
+
rows.push(...pluginKeys.flatMap((key) => pluginHelpRow(key.help)));
|
|
94
|
+
const combining = (combines) => terms
|
|
95
|
+
.filter((term) => term.combines === combines)
|
|
96
|
+
.map((term) => term.key)
|
|
97
|
+
.join(", ");
|
|
98
|
+
return [
|
|
99
|
+
"Query terms AND together. Terms:",
|
|
100
|
+
...rows,
|
|
101
|
+
...helpRow('WORD or "some phrase"', [
|
|
102
|
+
"case-insensitive substring of the title,",
|
|
103
|
+
"description, or any comment body; also the",
|
|
104
|
+
"entity's own ID, from four characters",
|
|
105
|
+
]),
|
|
106
|
+
`Same-key terms OR for single-valued fields (${combining("or")})`,
|
|
107
|
+
`and AND for multi-valued ones (${combining("and")}).`,
|
|
108
|
+
// A manifest says what its term means but not how it combines, so the
|
|
109
|
+
// sentence above cannot name it; its own description has to.
|
|
110
|
+
...(pluginKeys.length > 0 ? ["A plugin's terms combine as their descriptions say."] : []),
|
|
111
|
+
"The default query is status:open.",
|
|
112
|
+
].join("\n");
|
|
113
|
+
}
|
|
47
114
|
/**
|
|
48
115
|
* Help for `--commit`, naming the subject the verb commits under (spec 03 §3.2).
|
|
49
116
|
*
|
|
50
|
-
* The argument is the commit's scope rather than an entity kind:
|
|
51
|
-
*
|
|
117
|
+
* The argument is the commit's scope rather than an entity kind: a plugin
|
|
118
|
+
* declares scopes of its own (spec 03 §3.2), and those are deliberately not
|
|
119
|
+
* kinds.
|
|
52
120
|
*/
|
|
53
121
|
function commitHelp(scope) {
|
|
54
122
|
return `wrap the change in a 'docs${scope ? `(${scope})` : ""}:' commit`;
|
|
@@ -65,7 +133,45 @@ function commitHelp(scope) {
|
|
|
65
133
|
function withoutHelpVerb(command) {
|
|
66
134
|
return command.helpCommand(false);
|
|
67
135
|
}
|
|
68
|
-
|
|
136
|
+
/**
|
|
137
|
+
* The nouns and utilities this CLI defines, which a plugin may not take.
|
|
138
|
+
*
|
|
139
|
+
* Kept as data because two things read it: the collision check that refuses a
|
|
140
|
+
* plugin claiming one of these (spec 04 §4.3), and completion, which offers
|
|
141
|
+
* them alongside whatever plugins added.
|
|
142
|
+
*/
|
|
143
|
+
export const BUILTIN_NOUNS = [
|
|
144
|
+
"issue",
|
|
145
|
+
"pr",
|
|
146
|
+
"plugin",
|
|
147
|
+
"init",
|
|
148
|
+
"id",
|
|
149
|
+
"doctor",
|
|
150
|
+
"install",
|
|
151
|
+
"uninstall",
|
|
152
|
+
];
|
|
153
|
+
/**
|
|
154
|
+
* The top-level commands that do not warn about a newer tree: `doctor` reports
|
|
155
|
+
* it as D16 itself, and the rest set the repository up or print something
|
|
156
|
+
* that does not depend on the format.
|
|
157
|
+
*/
|
|
158
|
+
const FORMAT_INDEPENDENT = new Set([
|
|
159
|
+
"init",
|
|
160
|
+
"id",
|
|
161
|
+
"doctor",
|
|
162
|
+
"install",
|
|
163
|
+
"uninstall",
|
|
164
|
+
"plugin",
|
|
165
|
+
"__complete",
|
|
166
|
+
]);
|
|
167
|
+
/** Whether a command, by its top-level name, answers from the tree. */
|
|
168
|
+
function readsTheTree(action) {
|
|
169
|
+
let top = action;
|
|
170
|
+
while (top.parent?.parent)
|
|
171
|
+
top = top.parent;
|
|
172
|
+
return !FORMAT_INDEPENDENT.has(top.name());
|
|
173
|
+
}
|
|
174
|
+
export function buildProgram(getCtx, plugins) {
|
|
69
175
|
const program = withoutHelpVerb(new Command());
|
|
70
176
|
program
|
|
71
177
|
.name("nav")
|
|
@@ -73,6 +179,22 @@ export function buildProgram(getCtx) {
|
|
|
73
179
|
.version(VERSION, "-V, --version")
|
|
74
180
|
.showHelpAfterError()
|
|
75
181
|
.enablePositionalOptions();
|
|
182
|
+
// Before any command that reads or writes the tree, and before it can fail:
|
|
183
|
+
// an answer from a tool older than the tree is a guess, and the person
|
|
184
|
+
// reading it should know that first (spec 04 §4.3, D16).
|
|
185
|
+
program.hook("preAction", (_program, action) => {
|
|
186
|
+
if (!readsTheTree(action))
|
|
187
|
+
return;
|
|
188
|
+
let ctx;
|
|
189
|
+
try {
|
|
190
|
+
ctx = getCtx();
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return; // the action will report what is wrong with the context
|
|
194
|
+
}
|
|
195
|
+
if (ctx.hasNavbook)
|
|
196
|
+
warnNewerFormat(ctx, readMarkerVersion(ctx));
|
|
197
|
+
});
|
|
76
198
|
program
|
|
77
199
|
.command("init")
|
|
78
200
|
.description("create the Navbook skeleton at the repository root")
|
|
@@ -81,7 +203,7 @@ export function buildProgram(getCtx) {
|
|
|
81
203
|
program
|
|
82
204
|
.command("id")
|
|
83
205
|
.description("mint and print a fresh Navbook ID")
|
|
84
|
-
.option("-n, --count <n>", "how many IDs to print", (
|
|
206
|
+
.option("-n, --count <n>", "how many IDs to print", wholeNumber("IDs", 1), 1)
|
|
85
207
|
.action((opts) => cmdId(getCtx(), opts));
|
|
86
208
|
program
|
|
87
209
|
.command("doctor")
|
|
@@ -112,13 +234,91 @@ export function buildProgram(getCtx) {
|
|
|
112
234
|
.command("__complete", { hidden: true })
|
|
113
235
|
.description("internal: print completion candidates for the words typed so far")
|
|
114
236
|
.argument("[words...]")
|
|
115
|
-
.action((words) => cmdComplete(getCtx(), words));
|
|
116
|
-
program.addCommand(
|
|
117
|
-
program.addCommand(
|
|
118
|
-
program.addCommand(
|
|
237
|
+
.action(async (words) => cmdComplete(getCtx(), words, plugins));
|
|
238
|
+
program.addCommand(buildPluginCommand(getCtx));
|
|
239
|
+
program.addCommand(buildIssueCommand(getCtx, plugins));
|
|
240
|
+
program.addCommand(buildPrCommand(getCtx, plugins));
|
|
241
|
+
// Built from manifests alone: no plugin code is imported here, which is what
|
|
242
|
+
// lets `nav --help` cost the same with plugins installed as without
|
|
243
|
+
// (spec 04 §4.3).
|
|
244
|
+
for (const [, { plugin, spec }] of plugins?.commands.nouns ?? []) {
|
|
245
|
+
program.addCommand(buildDeclaredCommand(plugin, spec, getCtx, (target, path, args, opts) => plugins.run(getCtx(), target, path, args, opts)));
|
|
246
|
+
}
|
|
247
|
+
warnUnknownContributions(program, getCtx, plugins);
|
|
119
248
|
return program;
|
|
120
249
|
}
|
|
121
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Say which contributions name a command that does not exist.
|
|
252
|
+
*
|
|
253
|
+
* Checked once the whole tree is built, so every noun — built-in, shared verb
|
|
254
|
+
* or another plugin's — has had its chance to be the target. A contribution
|
|
255
|
+
* that matches nothing does nothing, and a plugin author who typed
|
|
256
|
+
* `issue lsit` deserves to hear that rather than wonder where their option went.
|
|
257
|
+
*/
|
|
258
|
+
function warnUnknownContributions(program, getCtx, plugins) {
|
|
259
|
+
for (const [on, entries] of plugins?.commands.contributions ?? []) {
|
|
260
|
+
let command = program;
|
|
261
|
+
for (const word of on.split(" ").filter(Boolean)) {
|
|
262
|
+
command = command?.commands.find((candidate) => candidate.name() === word);
|
|
263
|
+
}
|
|
264
|
+
if (command !== undefined && command !== program)
|
|
265
|
+
continue;
|
|
266
|
+
for (const { plugin } of entries) {
|
|
267
|
+
stderrOf(getCtx).write(`nav: plugin ${plugin.name} contributes to '${on}', which is not a nav command\n`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Where to warn while the command tree is being built.
|
|
273
|
+
*
|
|
274
|
+
* The context's stream when there is one; the process's otherwise, because
|
|
275
|
+
* building a context needs a repository and `nav id` outside one must not fail
|
|
276
|
+
* over a warning about a plugin.
|
|
277
|
+
*/
|
|
278
|
+
function stderrOf(getCtx) {
|
|
279
|
+
try {
|
|
280
|
+
return getCtx().stderr;
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return process.stderr;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* `nav plugin` — the store verbs of spec 04 §4.3.
|
|
288
|
+
*
|
|
289
|
+
* The store is per user, not per repository, so none of these needs one:
|
|
290
|
+
* installing a plugin from a home directory is as ordinary as installing it
|
|
291
|
+
* from a checkout. Inside a repository they still read its declaration.
|
|
292
|
+
*/
|
|
293
|
+
function buildPluginCommand(getRepoCtx) {
|
|
294
|
+
const getCtx = () => getRepoCtx({ requireRepo: false });
|
|
295
|
+
const plugin = withoutHelpVerb(new Command("plugin")).description("install and manage plugins");
|
|
296
|
+
plugin
|
|
297
|
+
.command("install")
|
|
298
|
+
.argument("[name...]", "packages to install; the repository's declaration otherwise")
|
|
299
|
+
.description("install plugins into the per-user store")
|
|
300
|
+
.option("-y, --yes", "do not ask for confirmation")
|
|
301
|
+
.action((names, opts) => cmdPluginInstall(getCtx(), names, opts));
|
|
302
|
+
plugin
|
|
303
|
+
.command("remove")
|
|
304
|
+
.argument("<name...>", "packages to remove")
|
|
305
|
+
.description("remove plugins from the store")
|
|
306
|
+
.option("-y, --yes", "do not ask for confirmation")
|
|
307
|
+
.action((names, opts) => cmdPluginRemove(getCtx(), names, opts));
|
|
308
|
+
plugin
|
|
309
|
+
.command("update")
|
|
310
|
+
.argument("[name...]", "packages to update; all of them otherwise")
|
|
311
|
+
.description("update installed plugins")
|
|
312
|
+
.option("-y, --yes", "do not ask for confirmation")
|
|
313
|
+
.action((names, opts) => cmdPluginUpdate(getCtx(), names, opts));
|
|
314
|
+
plugin
|
|
315
|
+
.command("list")
|
|
316
|
+
.description("what is installed, and whether this repository declares it")
|
|
317
|
+
.option("--json", "one JSON object per plugin, newline-delimited")
|
|
318
|
+
.action((opts) => cmdPluginList(getCtx(), opts));
|
|
319
|
+
return plugin;
|
|
320
|
+
}
|
|
321
|
+
function buildPrCommand(getCtx, plugins) {
|
|
122
322
|
const pr = withoutHelpVerb(new Command("pr")).description("work with pull requests");
|
|
123
323
|
pr.command("open")
|
|
124
324
|
.description("open a pull request from the current branch")
|
|
@@ -130,12 +330,12 @@ function buildPrCommand(getCtx) {
|
|
|
130
330
|
.option("--assignee <email>", "assign to a person (repeatable)", collect, [])
|
|
131
331
|
.option("--reviewer <email>", "ask a person to review it (repeatable)", collect, [])
|
|
132
332
|
.option("--milestone <name>", "milestone")
|
|
133
|
-
.option("--feature <slug>", "attach it to a feature (repeatable)", collect, [])
|
|
134
333
|
.option("--commit", commitHelp("pr"))
|
|
135
|
-
.action((opts) => cmdPrOpen(getCtx(), opts));
|
|
334
|
+
.action(async (opts) => cmdPrOpen(getCtx(), { ...opts, ext: await openFields(getCtx(), plugins, "pr open", opts) }));
|
|
136
335
|
pr.command("update")
|
|
137
336
|
.argument("<id>", "ID or unambiguous prefix")
|
|
138
337
|
.description("append a revision pinning the current HEAD")
|
|
338
|
+
.option("-y, --yes", YES_HELP)
|
|
139
339
|
.option("--commit", commitHelp("pr"))
|
|
140
340
|
.action((id, opts) => cmdPrUpdate(getCtx(), id, opts));
|
|
141
341
|
pr.command("request")
|
|
@@ -143,6 +343,7 @@ function buildPrCommand(getCtx) {
|
|
|
143
343
|
.argument("<email...>", "who to ask")
|
|
144
344
|
.description("ask people to review a pull request")
|
|
145
345
|
.option("--remove", "take them off the reviewers instead")
|
|
346
|
+
.option("-y, --yes", YES_HELP)
|
|
146
347
|
.option("--commit", commitHelp("pr"))
|
|
147
348
|
.action((id, people, opts) => cmdPrRequest(getCtx(), id, people, opts));
|
|
148
349
|
pr.command("review")
|
|
@@ -155,6 +356,7 @@ function buildPrCommand(getCtx) {
|
|
|
155
356
|
.option("--revision <sha>", "bind to this revision instead of the latest")
|
|
156
357
|
.option("--file <path>", "anchor the comment to a file")
|
|
157
358
|
.option("--line <n|start-end>", "anchor the comment to a line or range")
|
|
359
|
+
.option("-y, --yes", YES_HELP)
|
|
158
360
|
.option("--commit", commitHelp("pr"))
|
|
159
361
|
.action((id, opts) => cmdPrReview(getCtx(), id, opts));
|
|
160
362
|
pr.command("merge")
|
|
@@ -166,6 +368,7 @@ function buildPrCommand(getCtx) {
|
|
|
166
368
|
.option("--no-sync-source", "leave the source branch behind instead of fast-forwarding it")
|
|
167
369
|
.action((id, opts) => cmdPrMerge(getCtx(), id, opts));
|
|
168
370
|
addSharedVerbs(pr, "pr", getCtx, {
|
|
371
|
+
...(plugins ? { plugins, verbPrefix: "pr" } : {}),
|
|
169
372
|
// No extra columns here: `cmdPrList` owns the PR listing's columns, because
|
|
170
373
|
// it appends a `refs` one when scanning across branches.
|
|
171
374
|
extraColumns: [],
|
|
@@ -175,74 +378,12 @@ function buildPrCommand(getCtx) {
|
|
|
175
378
|
},
|
|
176
379
|
runList: (ctx, terms, options) => cmdPrList(ctx, terms, options),
|
|
177
380
|
});
|
|
381
|
+
// After the shared verbs, which are as much this noun's commands as `open`:
|
|
382
|
+
// an option contributed to `pr list` has to find `list` already there.
|
|
383
|
+
applyContributedOptions(pr, "pr", plugins, getCtx);
|
|
178
384
|
return pr;
|
|
179
385
|
}
|
|
180
|
-
|
|
181
|
-
* `nav feature` — spec 04 §4.3.
|
|
182
|
-
*
|
|
183
|
-
* None of the shared verbs appear here. A feature does not open and close, and
|
|
184
|
-
* it is not discussed: the discussion belongs to the issues attached to it. So
|
|
185
|
-
* the family is small on purpose, and `spec` groups what acts on the documents
|
|
186
|
-
* rather than on the feature itself.
|
|
187
|
-
*/
|
|
188
|
-
function buildFeatureCommand(getCtx) {
|
|
189
|
-
const feature = withoutHelpVerb(new Command("feature")).description("work with features");
|
|
190
|
-
feature
|
|
191
|
-
.command("open")
|
|
192
|
-
.argument("<title>", "one-line name for the feature")
|
|
193
|
-
.description("create a feature")
|
|
194
|
-
.option("-m, --message <text>", "summary text; without it $EDITOR is opened")
|
|
195
|
-
.option("--slug <slug>", "directory name to file it under; derived from the title otherwise")
|
|
196
|
-
.option("--commit", commitHelp("feature"))
|
|
197
|
-
.action((title, opts) => cmdFeatureOpen(getCtx(), title, opts));
|
|
198
|
-
feature
|
|
199
|
-
.command("list")
|
|
200
|
-
.description("list features, with how much work is attached to each")
|
|
201
|
-
.option("--json", "one JSON object per feature, newline-delimited")
|
|
202
|
-
.action((opts) => cmdFeatureList(getCtx(), opts));
|
|
203
|
-
feature
|
|
204
|
-
.command("show")
|
|
205
|
-
.argument("<slug>", "the feature's directory name")
|
|
206
|
-
.description("render a feature, its documents, and what has touched it")
|
|
207
|
-
.option("--json", "emit a single JSON object naming its issues and pull requests")
|
|
208
|
-
.option("--commits <n>", "recent commits to list (default 10)", (value) => value.trim() === "" ? Number.NaN : Number(value))
|
|
209
|
-
.action((slug, opts) => cmdFeatureShow(getCtx(), slug, opts));
|
|
210
|
-
feature
|
|
211
|
-
.command("edit")
|
|
212
|
-
.argument("<slug>", "the feature's directory name")
|
|
213
|
-
.description("open the feature's feature.md in $EDITOR")
|
|
214
|
-
.option("--commit", commitHelp("feature"))
|
|
215
|
-
.action((slug, opts) => cmdFeatureEdit(getCtx(), slug, opts));
|
|
216
|
-
feature.addCommand(buildSpecCommand(getCtx));
|
|
217
|
-
return feature;
|
|
218
|
-
}
|
|
219
|
-
function buildSpecCommand(getCtx) {
|
|
220
|
-
const spec = withoutHelpVerb(new Command("spec")).description("work with a feature's specification documents");
|
|
221
|
-
spec
|
|
222
|
-
.command("add")
|
|
223
|
-
.argument("<slug>", "the feature's directory name")
|
|
224
|
-
.argument("<title>", "one-line name for the document")
|
|
225
|
-
.description("add a specification document to a feature")
|
|
226
|
-
.option("-m, --message <text>", "document text; without it $EDITOR is opened")
|
|
227
|
-
.option("--file <name>", "file to write it to; derived from the title otherwise")
|
|
228
|
-
.option("--commit", commitHelp("feature"))
|
|
229
|
-
.action((slug, title, opts) => cmdSpecAdd(getCtx(), slug, title, opts));
|
|
230
|
-
spec
|
|
231
|
-
.command("edit")
|
|
232
|
-
.argument("<slug>", "the feature's directory name")
|
|
233
|
-
.argument("<file>", "the document's file name")
|
|
234
|
-
.description("open a specification document in $EDITOR")
|
|
235
|
-
.option("--commit", commitHelp("feature"))
|
|
236
|
-
.action((slug, file, opts) => cmdSpecEdit(getCtx(), slug, file, opts));
|
|
237
|
-
spec
|
|
238
|
-
.command("list")
|
|
239
|
-
.argument("<slug>", "the feature's directory name")
|
|
240
|
-
.description("list a feature's specification documents")
|
|
241
|
-
.option("--json", "one JSON object per document, newline-delimited")
|
|
242
|
-
.action((slug, opts) => cmdSpecList(getCtx(), slug, opts));
|
|
243
|
-
return spec;
|
|
244
|
-
}
|
|
245
|
-
function buildIssueCommand(getCtx) {
|
|
386
|
+
function buildIssueCommand(getCtx, plugins) {
|
|
246
387
|
const issue = withoutHelpVerb(new Command("issue")).description("work with issues");
|
|
247
388
|
issue
|
|
248
389
|
.command("open")
|
|
@@ -252,15 +393,14 @@ function buildIssueCommand(getCtx) {
|
|
|
252
393
|
.option("--label <label>", "add a label (repeatable)", collect, [])
|
|
253
394
|
.option("--assignee <email>", "assign to a person (repeatable)", collect, [])
|
|
254
395
|
.option("--milestone <name>", "milestone")
|
|
255
|
-
.option("--
|
|
256
|
-
// Number rather than parseInt, and blank rather than 0, for the reason
|
|
257
|
-
// `--depth` does it: a value the command must refuse has to reach it
|
|
258
|
-
// intact rather than arrive silently rounded or defaulted.
|
|
259
|
-
.option("--rank <n>", "where it sits in the queue; lower first", (value) => value.trim() === "" ? Number.NaN : Number(value))
|
|
396
|
+
.option("--rank <n>", "where it sits in the queue; lower first", finiteNumber)
|
|
260
397
|
.option("--deadline <date>", "when the work is wanted, YYYY-MM-DD")
|
|
261
398
|
.option("--parent <id>", "file it as a subtask of an existing issue")
|
|
262
399
|
.option("--commit", commitHelp("issue"))
|
|
263
|
-
.action((title, opts) => cmdIssueOpen(getCtx(), title,
|
|
400
|
+
.action(async (title, opts) => cmdIssueOpen(getCtx(), title, {
|
|
401
|
+
...opts,
|
|
402
|
+
ext: await openFields(getCtx(), plugins, "issue open", opts),
|
|
403
|
+
}));
|
|
264
404
|
issue
|
|
265
405
|
.command("link")
|
|
266
406
|
.argument("<id>", "ID or unambiguous prefix")
|
|
@@ -276,17 +416,44 @@ function buildIssueCommand(getCtx) {
|
|
|
276
416
|
.option("--commit", commitHelp("issue"))
|
|
277
417
|
.action((id, opts) => cmdIssueUnlink(getCtx(), id, opts));
|
|
278
418
|
addSharedVerbs(issue, "issue", getCtx, {
|
|
419
|
+
...(plugins ? { plugins, verbPrefix: "issue" } : {}),
|
|
279
420
|
extraColumns: [],
|
|
280
421
|
configureList: (command) => command.option("--sort <order>", `listing order: ${SORT_ORDERS.join(", ")} (default ${DEFAULT_SORT})`),
|
|
281
422
|
configureShow: (command) =>
|
|
282
|
-
// Number, not parseInt: '2.5' has to reach the command as 2.5 so it can
|
|
283
|
-
// be refused, rather than being silently rounded to something valid.
|
|
284
423
|
// No default here — `cmdShow` owns it, so every caller gets the same one.
|
|
285
|
-
command.option("--depth <n>", "levels of subtasks to render (default 1)", (
|
|
424
|
+
command.option("--depth <n>", "levels of subtasks to render (default 1)", wholeNumber("levels")),
|
|
286
425
|
configureDelete: (command) => command.option("-r, --recursive", "delete its subtasks too, to any depth"),
|
|
287
426
|
});
|
|
427
|
+
// After the shared verbs, for the reason `buildPrCommand` gives.
|
|
428
|
+
applyContributedOptions(issue, "issue", plugins, getCtx);
|
|
288
429
|
return issue;
|
|
289
430
|
}
|
|
431
|
+
/** What plugins contributed to one of this noun's verbs, loading them if any. */
|
|
432
|
+
async function verbHandlers(ctx, shared, verb) {
|
|
433
|
+
if (shared.plugins === undefined || shared.verbPrefix === undefined)
|
|
434
|
+
return [];
|
|
435
|
+
return shared.plugins.handlersFor(ctx, `${shared.verbPrefix} ${verb}`);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* One `jsonExtra` from several, or undefined when no plugin contributed one.
|
|
439
|
+
*
|
|
440
|
+
* A key the entity's own JSON already has is dropped: `title`, `status` and
|
|
441
|
+
* every frontmatter key are the format's answer, and the one a reader of
|
|
442
|
+
* `nav --json` is entitled to (spec 04 §4.2). A plugin adds keys; it does not
|
|
443
|
+
* get to quietly replace the built-in ones.
|
|
444
|
+
*/
|
|
445
|
+
function mergedJsonExtra(handlers) {
|
|
446
|
+
const contributors = handlers.filter((handler) => handler.jsonExtra !== undefined);
|
|
447
|
+
if (contributors.length === 0)
|
|
448
|
+
return undefined;
|
|
449
|
+
return (entity) => {
|
|
450
|
+
const merged = Object.assign({}, ...contributors.map((handler) => handler.jsonExtra?.(entity) ?? {}));
|
|
451
|
+
// Which keys exist does not depend on the Navbook directory's name.
|
|
452
|
+
for (const key of Object.keys(entityJson("", entity)))
|
|
453
|
+
delete merged[key];
|
|
454
|
+
return merged;
|
|
455
|
+
};
|
|
456
|
+
}
|
|
290
457
|
/** Register the verbs both nouns share, so their behavior can never drift. */
|
|
291
458
|
export function addSharedVerbs(parent, kind, getCtx, shared) {
|
|
292
459
|
const noun = kind === "issue" ? "issue" : "pull request";
|
|
@@ -295,31 +462,55 @@ export function addSharedVerbs(parent, kind, getCtx, shared) {
|
|
|
295
462
|
.command("list")
|
|
296
463
|
.argument("[query...]", "query terms; default status:open")
|
|
297
464
|
.description(`list ${kind === "issue" ? "issues" : "pull requests"} matching a query`)
|
|
298
|
-
.addHelpText("after", `\n${
|
|
465
|
+
.addHelpText("after", `\n${queryHelp(kind, shared.plugins?.queryKeys(kind))}`)
|
|
299
466
|
.option("--json", "one JSON object per entity, newline-delimited");
|
|
300
467
|
shared.configureList?.(list);
|
|
301
|
-
list.action((terms, opts) =>
|
|
302
|
-
|
|
303
|
-
|
|
468
|
+
list.action(async (terms, opts) => {
|
|
469
|
+
const contributed = await verbHandlers(getCtx(), shared, "list");
|
|
470
|
+
const columns = [...extraColumns, ...contributed.flatMap((h) => h.columns ?? [])];
|
|
471
|
+
const jsonExtra = mergedJsonExtra(contributed);
|
|
472
|
+
const options = { ...opts, extraColumns: columns, ...(jsonExtra ? { jsonExtra } : {}) };
|
|
473
|
+
if (shared.runList)
|
|
474
|
+
shared.runList(getCtx(), terms, options);
|
|
475
|
+
else
|
|
476
|
+
cmdList(getCtx(), kind, terms, options);
|
|
477
|
+
});
|
|
304
478
|
const show = parent
|
|
305
479
|
.command("show")
|
|
306
480
|
.argument("<id>", "ID or unambiguous prefix")
|
|
307
481
|
.description(`render one ${noun} and its comments`)
|
|
308
482
|
.option("--json", "emit a single JSON object including comments");
|
|
309
483
|
shared.configureShow?.(show);
|
|
310
|
-
show.action((id, opts) =>
|
|
311
|
-
|
|
484
|
+
show.action(async (id, opts) => {
|
|
485
|
+
const contributed = await verbHandlers(getCtx(), shared, "show");
|
|
486
|
+
const jsonExtra = mergedJsonExtra(contributed);
|
|
487
|
+
const sections = contributed.flatMap((handlers) => handlers.showSection ? [handlers.showSection] : []);
|
|
488
|
+
cmdShow(getCtx(), kind, id, {
|
|
489
|
+
...opts,
|
|
490
|
+
...(jsonExtra ? { jsonExtra } : {}),
|
|
491
|
+
...(sections.length > 0 ? { sections } : {}),
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
// An issue lives on whatever branch you are standing on, so there is never
|
|
495
|
+
// another worktree to send its write to: the flag is a pull-request notion.
|
|
496
|
+
const edit = parent
|
|
312
497
|
.command("edit")
|
|
313
498
|
.argument("<id>", "ID or unambiguous prefix")
|
|
314
|
-
.description(`open the ${noun}'s file in $EDITOR`)
|
|
499
|
+
.description(`open the ${noun}'s file in $EDITOR`);
|
|
500
|
+
if (kind === "pr")
|
|
501
|
+
edit.option("-y, --yes", YES_HELP);
|
|
502
|
+
edit
|
|
315
503
|
.option("--commit", commitHelp(kind))
|
|
316
504
|
.action((id, opts) => cmdEdit(getCtx(), kind, id, opts));
|
|
317
|
-
parent
|
|
505
|
+
const comment = parent
|
|
318
506
|
.command("comment")
|
|
319
507
|
.argument("<id>", "ID or unambiguous prefix")
|
|
320
508
|
.description(`add a comment to a ${noun}`)
|
|
321
509
|
.option("-m, --message <text>", "comment text; without it $EDITOR is opened")
|
|
322
|
-
.option("--reply-to <comment-id>", "comment this one replies to")
|
|
510
|
+
.option("--reply-to <comment-id>", "comment this one replies to");
|
|
511
|
+
if (kind === "pr")
|
|
512
|
+
comment.option("-y, --yes", YES_HELP);
|
|
513
|
+
comment
|
|
323
514
|
.option("--commit", commitHelp(kind))
|
|
324
515
|
.action((id, opts) => cmdComment(getCtx(), kind, id, opts));
|
|
325
516
|
parent
|
|
@@ -354,5 +545,52 @@ export function addSharedVerbs(parent, kind, getCtx, shared) {
|
|
|
354
545
|
export function collect(value, previous) {
|
|
355
546
|
return [...previous, value];
|
|
356
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* Add the options plugins declared for one verb, refusing a collision.
|
|
550
|
+
*
|
|
551
|
+
* From the manifest, so no plugin code runs: a `--feature` on `issue open`
|
|
552
|
+
* exists in `--help` and in completion whether or not anybody has run the
|
|
553
|
+
* command that would load the plugin implementing it.
|
|
554
|
+
*/
|
|
555
|
+
function applyContributedOptions(noun, kind, plugins, getCtx) {
|
|
556
|
+
if (plugins === undefined)
|
|
557
|
+
return;
|
|
558
|
+
for (const [on, entries] of plugins.commands.contributions) {
|
|
559
|
+
const [target, verb] = on.split(" ");
|
|
560
|
+
if (target !== kind || verb === undefined)
|
|
561
|
+
continue;
|
|
562
|
+
const command = noun.commands.find((candidate) => candidate.name() === verb);
|
|
563
|
+
if (command === undefined)
|
|
564
|
+
continue;
|
|
565
|
+
for (const { plugin, spec } of entries) {
|
|
566
|
+
for (const option of spec.options ?? []) {
|
|
567
|
+
const problem = optionCollision(command, option);
|
|
568
|
+
if (problem !== null) {
|
|
569
|
+
// Before `.option()`, which throws on a duplicate flag: the plugin
|
|
570
|
+
// loses its option and says so, rather than taking the CLI down.
|
|
571
|
+
stderrOf(getCtx).write(`nav: plugin ${plugin.name} option skipped on '${on}': ${problem}\n`);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
applyOption(command, option);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Frontmatter a plugin wants on a newly opened entity, read off its options.
|
|
581
|
+
*
|
|
582
|
+
* This is the one contribution that loads plugin code on a built-in verb, and
|
|
583
|
+
* only when a plugin declared a contribution to *this* verb — so `nav issue
|
|
584
|
+
* open` with no such plugin imports nothing.
|
|
585
|
+
*/
|
|
586
|
+
async function openFields(ctx, plugins, verb, opts) {
|
|
587
|
+
if (plugins === undefined || !plugins.commands.contributions.has(verb))
|
|
588
|
+
return undefined;
|
|
589
|
+
const fields = {};
|
|
590
|
+
for (const handlers of await plugins.handlersFor(ctx, verb)) {
|
|
591
|
+
Object.assign(fields, handlers.openFields?.(opts) ?? {});
|
|
592
|
+
}
|
|
593
|
+
return Object.keys(fields).length > 0 ? fields : undefined;
|
|
594
|
+
}
|
|
357
595
|
export { Option };
|
|
358
596
|
//# sourceMappingURL=program.js.map
|