Skip to content

Commit 024f776

Browse files
committed
CLI: nested, sorted command tree in --help + integrations pointer (RND-12047, RND-12053)
- Append a "Command groups (nested)" section to every group's --help so nested subgroups (e.g. organizations sites) are discoverable without drilling in. Root uses a compact one-line-per-group layout; subgroup pages use an indented tree. Attached generically by walking the Commander tree at runtime. - Sort subcommands alphabetically (built-in list via configureHelp + the tree). - Point `gitbook integrations --help` at the singular `gitbook integration` group for the build/publish lifecycle (new/dev/publish/...).
1 parent 7756334 commit 024f776

3 files changed

Lines changed: 162 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gitbook/cli': patch
3+
---
4+
5+
Improve command discoverability: `--help` now shows a nested "Command groups" tree so subgroups like `organizations sites` are visible without drilling in; shell completion now includes the hand-written `integration` and `openapi` subcommands; and `gitbook integrations --help` points to `gitbook integration` for the build/publish lifecycle commands.

packages/cli/src/cli.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,14 @@ import { authenticate, login, logout, whoami } from './remote';
3232
import { withEnvironment } from './environments';
3333
import { registerGeneratedCommands, COMPLETIONS } from './generated-commands';
3434
import { registerCustomCommands } from './api-commands';
35+
import { installCommandTreeHelp } from './help-tree';
3536

3637
program.name('gitbook').description(packageJSON.description).version(packageJSON.version);
3738

39+
// List subcommands alphabetically in --help (both the built-in command list and
40+
// the nested tree in help-tree.ts), rather than in registration order.
41+
program.configureHelp({ sortSubcommands: true });
42+
3843
program
3944
.command('login')
4045
.option('-e, --endpoint <endpoint>', GITBOOK_DEFAULT_ENDPOINT)
@@ -153,6 +158,19 @@ program
153158
registerGeneratedCommands(program);
154159
registerCustomCommands(program);
155160

161+
// The integration build/publish lifecycle (new/dev/publish/…) lives under the
162+
// singular `integration` group, kept distinct from the spec-generated plural
163+
// `integrations` group (raw API ops). People reasonably look under `integrations`
164+
// first, so point them across from there.
165+
const integrationsGroup = program.commands.find((c) => c.name() === 'integrations');
166+
integrationsGroup?.addHelpText(
167+
'after',
168+
`\nTo create, run, or publish your own integration (new, dev, publish, …), see \`${program.name()} integration --help\`.`,
169+
);
170+
171+
// Reveal nested subgroups in `--help` (Commander shows only immediate children).
172+
installCommandTreeHelp(program);
173+
156174
checkNodeVersion({ node: '>= 18' }, (error, result) => {
157175
if (error) {
158176
console.error(error);

packages/cli/src/help-tree.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { Command, Help } from 'commander';
2+
3+
/**
4+
* Nested command-tree help.
5+
*
6+
* Commander's default `--help` only lists a command's *immediate* children, so
7+
* deeper subgroups (e.g. `organizations sites`, `spaces content pages`) are
8+
* invisible until you drill in one level at a time. This module appends a
9+
* "Command groups (nested)" section to every group's help that reveals the
10+
* nested subgroup structure, so you can discover that `organizations sites`
11+
* exists from `gitbook --help` without hunting.
12+
*
13+
* Two layouts, so the big root listing stays scannable without scrolling while
14+
* deeper pages read naturally:
15+
* - root `gitbook --help`: COMPACT — one line per top-level group, its
16+
* subgroups listed comma-separated (and wrapped) alongside it.
17+
* - any subgroup's `--help`: INDENTED — that group's subgroups one per line.
18+
*
19+
* It's attached uniformly by walking the Commander tree at runtime, so there's
20+
* nothing to keep in sync as commands change. Only *subgroups* are listed — leaf
21+
* verbs (list/get/create/…) are omitted to keep the tree structural; each
22+
* group's own `--help` still lists its verbs.
23+
*/
24+
25+
// How many further levels of nested subgroups the INDENTED (subgroup-page)
26+
// layout shows beneath the helped command's direct children. 1 reveals the
27+
// immediate subgroups without drilling in while keeping output bounded — depth 2
28+
// explodes on wide subtrees like `sites` (~30 nested collections).
29+
const TREE_DEPTH = 1;
30+
31+
const INDENT = ' ';
32+
33+
// A group's subgroups: visible children that themselves have visible children.
34+
// Leaf verb commands (and the implicit `help` command) have no children and are
35+
// excluded. Sorted alphabetically via the shared, sort-enabled Help instance.
36+
function subgroups(cmd: Command, helper: Help): Command[] {
37+
return helper.visibleCommands(cmd).filter((child) => helper.visibleCommands(child).length > 0);
38+
}
39+
40+
// ─── Indented layout (subgroup pages) ──────────────────────────────────────────
41+
42+
function renderIndented(
43+
cmd: Command,
44+
helper: Help,
45+
indent: number,
46+
depthRemaining: number,
47+
out: string[],
48+
): void {
49+
for (const child of subgroups(cmd, helper)) {
50+
out.push(`${INDENT.repeat(indent)}${child.name()}`);
51+
if (depthRemaining > 0) {
52+
renderIndented(child, helper, indent + 1, depthRemaining - 1, out);
53+
}
54+
}
55+
}
56+
57+
function indentedSection(cmd: Command, helper: Help): string {
58+
const out: string[] = [];
59+
renderIndented(cmd, helper, 1, TREE_DEPTH, out);
60+
return `\nCommand groups (nested):\n${out.join('\n')}\n`;
61+
}
62+
63+
// ─── Compact layout (root) ──────────────────────────────────────────────────--
64+
65+
// Greedily pack comma-separated tokens into lines no wider than `width`.
66+
function wrapTokens(tokens: string[], width: number): string[] {
67+
const lines: string[] = [];
68+
let cur = '';
69+
tokens.forEach((tok, i) => {
70+
const piece = i < tokens.length - 1 ? `${tok},` : tok;
71+
if (cur === '') {
72+
cur = piece;
73+
} else if (cur.length + 1 + piece.length <= width) {
74+
cur += ` ${piece}`;
75+
} else {
76+
lines.push(cur);
77+
cur = piece;
78+
}
79+
});
80+
if (cur) lines.push(cur);
81+
return lines;
82+
}
83+
84+
function compactSection(cmd: Command, helper: Help): string {
85+
const groups = subgroups(cmd, helper);
86+
const rows = groups.map((g) => ({
87+
name: g.name(),
88+
children: subgroups(g, helper).map((c) => c.name()),
89+
}));
90+
91+
const nameWidth = Math.max(...rows.map((r) => r.name.length));
92+
const valueCol = INDENT.length + nameWidth + 2;
93+
const termWidth = Math.min(process.stdout.columns || 80, 100);
94+
// Leave room for the value column; never wrap narrower than 20 cols.
95+
const wrapWidth = Math.max(20, termWidth - valueCol);
96+
97+
const lines: string[] = [];
98+
for (const row of rows) {
99+
const label = INDENT + row.name.padEnd(nameWidth + 2);
100+
if (row.children.length === 0) {
101+
lines.push(label.trimEnd());
102+
continue;
103+
}
104+
const wrapped = wrapTokens(row.children, wrapWidth);
105+
lines.push(label + wrapped[0]);
106+
for (let i = 1; i < wrapped.length; i++) {
107+
lines.push(' '.repeat(valueCol) + wrapped[i]);
108+
}
109+
}
110+
111+
return `\nCommand groups (nested):\n${lines.join('\n')}\n`;
112+
}
113+
114+
// ─── Wiring ─────────────────────────────────────────────────────────────────--
115+
116+
/**
117+
* Attach the nested-tree help section to `program` (compact) and every group
118+
* beneath it (indented).
119+
*/
120+
export function installCommandTreeHelp(program: Command): void {
121+
const helper = new Help();
122+
// Render subgroups alphabetically rather than in registration order.
123+
helper.sortSubcommands = true;
124+
125+
const attach = (cmd: Command, isRoot: boolean): void => {
126+
// Only groups with nested subgroups get a tree; a group whose children
127+
// are all leaf verbs has nothing extra to reveal.
128+
if (subgroups(cmd, helper).length > 0) {
129+
cmd.addHelpText('after', () =>
130+
isRoot ? compactSection(cmd, helper) : indentedSection(cmd, helper),
131+
);
132+
}
133+
for (const child of cmd.commands) {
134+
attach(child, false);
135+
}
136+
};
137+
138+
attach(program, true);
139+
}

0 commit comments

Comments
 (0)