-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathhelp.ts
320 lines (280 loc) · 10.4 KB
/
help.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
import { Command, Help, Config, Interfaces } from "@oclif/core";
import chalk from "chalk";
import stripAnsi from "strip-ansi";
import { ConfigManager } from "./services/config-manager.js";
import { displayLogo } from "./utils/logo.js";
import { WEB_CLI_RESTRICTED_COMMANDS } from "./base-command.js"; // Import the single source of truth
export default class CustomHelp extends Help {
static skipCache = true; // For development - prevents help commands from being cached
protected webCliMode: boolean;
protected configManager: ConfigManager;
// Flag to track if we're already showing root help to prevent duplication
protected isShowingRootHelp: boolean = false;
constructor(config: Config, opts?: Record<string, unknown>) {
super(config, opts);
this.webCliMode = process.env.ABLY_WEB_CLI_MODE === "true";
this.configManager = new ConfigManager();
}
// Override formatHelpOutput to apply stripAnsi when necessary
formatHelpOutput(output: string): string {
// Check if we're generating readme (passed as an option from oclif)
if (this.opts?.stripAnsi || process.env.GENERATING_README === "true") {
return stripAnsi(output);
}
return output;
}
// Helper to ensure no trailing whitespace
private removeTrailingWhitespace(text: string): string {
// Remove all trailing newlines completely
return text.replace(/\n+$/, "");
}
// Override the display method to clean up trailing whitespace and exit cleanly
async showHelp(argv: string[]): Promise<void> {
const command = this.config.findCommand(argv[0]);
if (!command) return super.showHelp(argv);
// Get formatted output
const output = this.formatCommand(command);
const cleanedOutput = this.removeTrailingWhitespace(output);
// Apply stripAnsi when needed
console.log(this.formatHelpOutput(cleanedOutput));
process.exit(0);
}
// Override for root help as well
async showRootHelp(): Promise<void> {
// Get formatted output
const output = this.formatRoot();
const cleanedOutput = this.removeTrailingWhitespace(output);
// Apply stripAnsi when needed
console.log(this.formatHelpOutput(cleanedOutput));
process.exit(0);
}
formatRoot(): string {
let output: string;
// Set flag to indicate we're showing root help
this.isShowingRootHelp = true;
const args = process.argv || [];
const isWebCliHelp = args.includes("web-cli") || args.includes("webcli");
if (this.webCliMode || isWebCliHelp) {
output = this.formatWebCliRoot();
} else {
output = this.formatStandardRoot();
}
return output; // Let the overridden render handle stripping
}
formatStandardRoot(): string {
// Manually construct root help (bypassing super.formatRoot)
const { config } = this;
const lines: string[] = [];
// 1. Logo (conditionally)
const logoLines: string[] = [];
if (process.stdout.isTTY) {
displayLogo((m: string) => logoLines.push(m)); // Use capture
}
lines.push(...logoLines);
// 2. Title & Usage
const headerLines = [
chalk.bold("ably.com CLI for Pub/Sub, Chat, Spaces and the Control API"),
"",
`${chalk.bold("USAGE")}`,
` $ ${config.bin} [COMMAND]`,
"",
chalk.bold("COMMANDS"), // Use the desired single heading
];
lines.push(...headerLines);
// 3. Get, filter, combine, sort, and format visible commands/topics
// Use a Map to ensure unique entries by command/topic name
const uniqueEntries = new Map();
// Process commands first
config.commands
.filter((c) => !c.hidden && !c.id.includes(":")) // Filter hidden and top-level only
.filter((c) => this.shouldDisplay(c)) // Apply web mode filtering
.forEach((c) => {
uniqueEntries.set(c.id, {
id: c.id,
description: c.description,
isCommand: true,
});
});
// Then add topics if they don't already exist as commands
config.topics
.filter((t) => !t.hidden && !t.name.includes(":")) // Filter hidden and top-level only
.filter((t) => this.shouldDisplay({ id: t.name } as Command.Loadable)) // Apply web mode filtering
.forEach((t) => {
if (!uniqueEntries.has(t.name)) {
uniqueEntries.set(t.name, {
id: t.name,
description: t.description,
isCommand: false,
});
}
});
// Convert to array and sort
const combined = [...uniqueEntries.values()].sort((a, b) => {
return a.id.localeCompare(b.id);
});
if (combined.length > 0) {
const commandListString = this.renderList(
combined.map((c) => {
const description =
c.description && this.render(c.description.split("\n")[0]);
const descString = description ? chalk.dim(description) : undefined;
return [chalk.cyan(c.id), descString];
}),
{ indentation: 2, spacer: "\n" }, // Adjust spacing if needed
);
lines.push(commandListString);
} else {
lines.push(" No commands found.");
}
// 4. Login prompt (if needed and not in web mode)
if (!this.webCliMode) {
const accessToken =
process.env.ABLY_ACCESS_TOKEN || this.configManager.getAccessToken();
const apiKey = process.env.ABLY_API_KEY;
if (!accessToken && !apiKey) {
lines.push(
"",
chalk.yellow(
"You are not logged in. Run the following command to log in:",
),
chalk.cyan(" $ ably accounts login"),
);
}
}
// Join lines and return
return lines.join("\n");
}
formatWebCliRoot(): string {
const lines: string[] = [];
if (process.stdout.isTTY) {
displayLogo((m: string) => lines.push(m)); // Add logo lines directly
}
lines.push(
chalk.bold(
"ably.com browser-based CLI for Pub/Sub, Chat, Spaces and the Control API",
),
"",
);
// 3. Show the web CLI specific instructions
const webCliCommands = [
`${chalk.bold("COMMON COMMANDS")}`,
` ${chalk.cyan("View Ably commands:")} ably --help`,
` ${chalk.cyan("Publish a message:")} ably channels publish [channel] [message]`,
` ${chalk.cyan("View live channel lifecycle events:")} ably channels logs`,
];
lines.push(...webCliCommands);
// 4. Check if login recommendation is needed
const accessToken =
process.env.ABLY_ACCESS_TOKEN || this.configManager.getAccessToken();
const apiKey = process.env.ABLY_API_KEY;
if (!accessToken && !apiKey) {
lines.push(
"",
chalk.yellow(
"You are not logged in. Run the following command to log in:",
),
chalk.cyan(" $ ably login"),
);
}
// Join lines and return
return lines.join("\n");
}
formatCommand(command: Command.Loadable): string {
let output: string;
// Special case handling for web-cli help command
if (command.id === "help:web-cli" || command.id === "help:webcli") {
this.isShowingRootHelp = true; // Prevent further sections
output = this.formatWebCliRoot();
} else {
// Reset root help flag when showing individual command help
this.isShowingRootHelp = false;
// Use super's formatCommand
output = super.formatCommand(command);
// Modify based on web CLI mode using the imported list
if (
this.webCliMode &&
WEB_CLI_RESTRICTED_COMMANDS.some(
(restricted) =>
command.id === restricted ||
command.id.startsWith(restricted + ":"),
)
) {
output = [
`${chalk.bold("This command is not available in the web CLI mode.")}`,
"",
"Please use the standalone CLI installation instead.",
].join("\n");
}
}
return output; // Let the overridden render handle stripping
}
// Re-add the check for web CLI mode command availability
shouldDisplay(command: Command.Loadable): boolean {
if (!this.webCliMode) {
return true; // Always display if not in web mode
}
// In web mode, check if the command should be hidden using the imported list
// Check if the commandId starts with any restricted command base
return !WEB_CLI_RESTRICTED_COMMANDS.some(
(restricted) =>
command.id === restricted || command.id.startsWith(restricted + ":"),
);
}
formatCommands(commands: Command.Loadable[]): string {
// Skip if we're already showing root help to prevent duplication
if (this.isShowingRootHelp) {
return "";
}
// Filter commands based on webCliMode using shouldDisplay
const visibleCommands = commands.filter((c) => this.shouldDisplay(c));
if (visibleCommands.length === 0) return ""; // Return empty if no commands should be shown
return this.section(
chalk.bold("COMMANDS"),
this.renderList(
visibleCommands.map((c) => {
const description =
c.description && this.render(c.description.split("\n")[0]);
return [
chalk.cyan(c.id),
description ? chalk.dim(description) : undefined,
];
}),
{ indentation: 2 },
),
);
}
formatTopics(topics: Interfaces.Topic[]): string {
// Skip if we're already showing root help to prevent duplication
if (this.isShowingRootHelp) {
return "";
}
// Filter topics based on webCliMode using shouldDisplay logic
const visibleTopics = topics.filter((t) => {
if (!this.webCliMode) return true;
// Check if the topic name itself is restricted or if any restricted command starts with the topic name
return !WEB_CLI_RESTRICTED_COMMANDS.some(
(restricted) =>
t.name === restricted ||
(restricted.startsWith(t.name + ":") && t.name !== "help"), // Check if command starts with topic: avoids hiding 'help'
);
});
if (visibleTopics.length === 0) return "";
return this.section(
chalk.bold("TOPICS"),
topics
.filter((t) => this.shouldDisplay({ id: t.name } as Command.Loadable)) // Reuse shouldDisplay logic
.map((c) => {
const description =
c.description && this.render(c.description.split("\n")[0]);
return [
chalk.cyan(c.name),
description ? chalk.dim(description) : undefined,
];
})
.map(([left, right]) =>
this.renderList([[left, right]], { indentation: 2 }),
)
.join("\n"),
);
}
}