Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/modern-carpets-lose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@calycode/cli": patch
---

chore: help command styling for easy skimming
133 changes: 20 additions & 113 deletions packages/cli/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Command, number>();

Expand All @@ -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
Expand Down Expand Up @@ -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 <command> [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 };
155 changes: 153 additions & 2 deletions packages/cli/src/utils/commands/main-program-utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { font } from '../methods/font';

function isDeprecated(cmd) {
const desc = cmd.description ? cmd.description() : '';
return desc.trim().startsWith('[DEPRECATED]');
Expand All @@ -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 = [];

Expand All @@ -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 <command> [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 };
4 changes: 4 additions & 0 deletions packages/cli/src/utils/methods/font.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,29 @@ 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: {
cyan,
yellowBright,
white,
gray,
green,
},
weight: {
bold,
},
combo: {
boldCyan,
boldGreen,
},
};

Expand Down