diff --git a/.changeset/modern-carpets-lose.md b/.changeset/modern-carpets-lose.md new file mode 100644 index 00000000..165c108b --- /dev/null +++ b/.changeset/modern-carpets-lose.md @@ -0,0 +1,5 @@ +--- +"@calycode/cli": patch +--- + +chore: help command styling for easy skimming diff --git a/packages/cli/src/program.ts b/packages/cli/src/program.ts index 8db87f19..f4757f82 100644 --- a/packages/cli/src/program.ts +++ b/packages/cli/src/program.ts @@ -13,7 +13,10 @@ import { Caly } from '@calycode/core'; import { InitializedPostHog } from './utils/posthog/init'; import { nodeConfigStorage } from './node-config-storage'; import { registerGenerateCommands } from './commands/generate'; -import { collectVisibleLeafCommands, getFullCommandPath } from './utils/commands/main-program-utils'; +import { + getFullCommandPath, + applyCustomHelpToAllCommands, +} from './utils/commands/main-program-utils'; const commandStartTimes = new WeakMap(); @@ -35,23 +38,23 @@ program.hook('preAction', (thisCommand, actionCommand) => { }); program.hook('postAction', (thisCommand, actionCommand) => { - const start = commandStartTimes.get(thisCommand); - if (!start) return; - const duration = ((Date.now() - start) / 1000).toFixed(2); + const start = commandStartTimes.get(thisCommand); + if (!start) return; + const duration = ((Date.now() - start) / 1000).toFixed(2); - const commandPath = getFullCommandPath(actionCommand); + const commandPath = getFullCommandPath(actionCommand); - console.log(`\n⏱️ Command "${commandPath}" completed in ${duration}s`); - InitializedPostHog.captureImmediate({ - distinctId: 'anonymous', - event: 'command_finished', - properties: { - $process_person_profile: false, - command: commandPath, - duration: duration, - }, - }); - InitializedPostHog.shutdown(); + console.log(`\n⏱️ Command "${commandPath}" completed in ${duration}s`); + InitializedPostHog.captureImmediate({ + distinctId: 'anonymous', + event: 'command_finished', + properties: { + $process_person_profile: false, + command: commandPath, + duration: duration, + }, + }); + InitializedPostHog.shutdown(); }); program @@ -95,102 +98,6 @@ registerTestCommands(program, core); registerContextCommands(program, core); // --- Custom Help Formatter --- -program.configureHelp({ - formatHelp(cmd, helper) { - // 1. Collect all visible leaf commands with their full paths - const allLeafCmds = collectVisibleLeafCommands(cmd); - - // 2. For alignment: determine the longest command path string - const allNames = allLeafCmds.map((c) => c.path.join(' ')); - const longestName = allNames.reduce((len, n) => Math.max(len, n.length), 0); - const pad = (str, len) => str + ' '.repeat(len - str.length); - - // 3. Define your desired groups (with full string paths) - const groups = [ - { - title: font.combo.boldCyan('Core Commands:'), - commands: ['init'], - }, - { - title: font.combo.boldCyan('Generation Commands:'), - commands: [ - 'generate spec', - 'generate codegen', - 'generate repo', - 'generate xanoscript', - 'generate docs', - ], - }, - { - title: font.combo.boldCyan('Registry:'), - commands: ['registry add', 'registry scaffold'], - }, - { - title: font.combo.boldCyan('Serve:'), - commands: ['serve spec', 'serve registry'], - }, - { - title: font.combo.boldCyan('Backups:'), - commands: ['backup export', 'backup restore'], - }, - { - title: font.combo.boldCyan('Testing & Linting:'), - commands: ['test run'], - }, - { - title: font.combo.boldCyan('Other:'), - commands: ['context show'], - }, - ]; - - // 4. Map full path strings to command objects - const cmdMap = Object.fromEntries(allLeafCmds.map((c) => [c.path.join(' '), c])); - - // 5. Track which commands are used - const used = new Set(groups.flatMap((g) => g.commands)); - const ungrouped = allLeafCmds.map((c) => c.path.join(' ')).filter((name) => !used.has(name)); - - if (ungrouped.length) { - groups.push({ - title: font.combo.boldCyan('Other:'), - commands: ungrouped, - }); - } - - // 6. Usage line - let output = [font.weight.bold(`\nUsage: xano [options]\n`)]; - - // Banner and description - if (cmd.description()) { - output.push(cmd.description() + '\n'); - } - - // Options - output.push(font.weight.bold('Options:')); - output.push( - ` -v, --version ${font.color.gray('output the version number')}\n` + - ` -h, --help ${font.color.gray('display help for command')}\n` - ); - - // 7. Command Groups - for (const group of groups) { - output.push('\n' + group.title); - for (const cname of group.commands) { - const c = cmdMap[cname]; - if (c) { - const opts = ' ' + font.color.gray('-h, --help'); - output.push( - ` ${font.weight.bold(font.color.yellowBright(pad(cname, longestName)))}${opts}\n ${c.description}\n` - ); - } - } - } - - // Footer/help link - output.push(font.color.gray('Need help? Visit https://github.com/calycode/xano-tools\n')); - - return output.join('\n'); - }, -}); +applyCustomHelpToAllCommands(program); export { program }; diff --git a/packages/cli/src/utils/commands/main-program-utils.ts b/packages/cli/src/utils/commands/main-program-utils.ts index 6961d3d5..90c63a1b 100644 --- a/packages/cli/src/utils/commands/main-program-utils.ts +++ b/packages/cli/src/utils/commands/main-program-utils.ts @@ -1,3 +1,5 @@ +import { font } from '../methods/font'; + function isDeprecated(cmd) { const desc = cmd.description ? cmd.description() : ''; return desc.trim().startsWith('[DEPRECATED]'); @@ -18,7 +20,6 @@ function getFullCommandPath(cmd) { } function collectVisibleLeafCommands(cmd, parentPath = []) { - const isDeprecated = (c) => c.description && c.description().trim().startsWith('[DEPRECATED]'); const path = [...parentPath, cmd.name()].filter((segment) => segment !== 'xano'); let results = []; @@ -42,4 +43,154 @@ function collectVisibleLeafCommands(cmd, parentPath = []) { return results; } -export { getFullCommandPath, collectVisibleLeafCommands }; +function customFormatHelp(cmd, helper) { + // 1. Banner and Description + let output = []; + if (cmd.description()) { + output.push(font.weight.bold(cmd.description())); + } + + // 2. Usage + output.push(font.weight.bold(`\nUsage: ${helper.commandUsage(cmd)}\n`)); + + // 3. Arguments + const argList = helper.visibleArguments(cmd); + if (argList.length) { + output.push(font.weight.bold('Arguments:')); + for (const arg of argList) { + output.push( + ` ${font.color.yellowBright(arg.name())}` + `\n ${arg.description || ''}\n` + ); + } + } + + // 4. Options + const optionsList = helper.visibleOptions(cmd); + if (optionsList.length) { + output.push(font.weight.bold('Options:')); + for (const opt of optionsList) { + output.push(` ${font.color.cyan(opt.flags)}` + `\n ${opt.description || ''}\n`); + } + } + + // 5. Subcommands + const subcommands = helper.visibleCommands(cmd); + if (subcommands.length) { + output.push(font.weight.bold('Commands:')); + for (const sub of subcommands) { + output.push(` ${font.color.green(sub.name())}` + `\n ${sub.description() || ''}\n`); + } + } + + // 6. Footer + output.push(font.color.gray('\nNeed help? Visit https://github.com/calycode/xano-tools\n')); + return output.join('\n'); +} + +function customFormatHelpForRoot(cmd) { + // 1. Collect all visible leaf commands with their full paths + const allLeafCmds = collectVisibleLeafCommands(cmd); + + // 2. For alignment: determine the longest command path string + const allNames = allLeafCmds.map((c) => c.path.join(' ')); + const longestName = allNames.reduce((len, n) => Math.max(len, n.length), 0); + const pad = (str, len) => str + ' '.repeat(len - str.length); + + // 3. Define your desired groups (with full string paths) + const groups = [ + { + title: font.combo.boldCyan('Core Commands:'), + commands: ['init'], + }, + { + title: font.combo.boldCyan('Generation Commands:'), + commands: ['generate codegen', 'generate docs', 'generate repo', 'generate spec'], + }, + { + title: font.combo.boldCyan('Registry:'), + commands: ['registry add', 'registry scaffold'], + }, + { + title: font.combo.boldCyan('Serve:'), + commands: ['serve spec', 'serve registry'], + }, + { + title: font.combo.boldCyan('Backups:'), + commands: ['backup export', 'backup restore'], + }, + { + title: font.combo.boldCyan('Testing & Linting:'), + commands: ['test run'], + }, + ]; + + // 4. Map full path strings to command objects + const cmdMap = Object.fromEntries(allLeafCmds.map((c) => [c.path.join(' '), c])); + + // 5. Track which commands are used + const used = new Set(groups.flatMap((g) => g.commands)); + const ungrouped = allLeafCmds.map((c) => c.path.join(' ')).filter((name) => !used.has(name)); + + if (ungrouped.length) { + groups.push({ + title: font.combo.boldCyan('Other:'), + commands: ungrouped, + }); + } + + // 6. Usage line + let output = [font.weight.bold(`\nUsage: xano [options]\n`)]; + + // Banner and description + if (cmd.description()) { + output.push(cmd.description() + '\n'); + } + + // Options + output.push(font.weight.bold('Options:')); + output.push( + ` -v, --version ${font.color.gray('output the version number')}\n` + + ` -h, --help ${font.color.gray('display help for command')}\n` + ); + + // 7. Command Groups + for (const group of groups) { + output.push('\n' + group.title); + for (const cname of group.commands) { + const c = cmdMap[cname]; + if (c) { + const opts = ' ' + font.color.gray('-h, --help'); + output.push( + ` ${font.weight.bold( + font.color.yellowBright(pad(cname, longestName)) + )}${opts}\n ${c.description}\n` + ); + } + } + } + + // Footer/help link + output.push( + font.color.gray( + 'Need help? Visit https://github.com/calycode/xano-tools or reach out to us on https://links.calycode.com/discord\n' + ) + ); + + return output.join('\n'); +} + +function applyCustomHelpToAllCommands(cmd) { + if (cmd.parent === null) { + cmd.configureHelp({ formatHelp: customFormatHelpForRoot }); + } else { + cmd.configureHelp({ formatHelp: customFormatHelp }); + } + + if (cmd.commands && cmd.commands.length > 0) { + for (const sub of cmd.commands) { + applyCustomHelpToAllCommands(sub); + } + } +} + +export { getFullCommandPath, applyCustomHelpToAllCommands }; diff --git a/packages/cli/src/utils/methods/font.ts b/packages/cli/src/utils/methods/font.ts index 3cb27ed3..47eaf074 100644 --- a/packages/cli/src/utils/methods/font.ts +++ b/packages/cli/src/utils/methods/font.ts @@ -7,12 +7,14 @@ const cyan = wrap(36, 39); const yellowBright = wrap(93, 39); const white = wrap(37, 39); const gray = wrap(90, 39); +const green = wrap(32, 39); // Weight const bold = wrap(1, 22); // Combined color + weight variants const boldCyan = (str: string) => bold(cyan(str)); +const boldGreen = (str: string) => bold(green(str)); const font = { color: { @@ -20,12 +22,14 @@ const font = { yellowBright, white, gray, + green, }, weight: { bold, }, combo: { boldCyan, + boldGreen, }, };