/ src / serialization.ts
serialization.ts
 1  /**
 2   * Serialization and formatting helpers for CLI commands and args.
 3   *
 4   * Used by the `list` command, Commander --help, and build-manifest.
 5   * Separated from registry.ts to keep the registry focused on types + registration.
 6   */
 7  
 8  import type { Arg, CliCommand } from './registry.js';
 9  import { fullName, strategyLabel } from './registry.js';
10  
11  // ── Serialization ───────────────────────────────────────────────────────────
12  
13  export type SerializedArg = {
14    name: string;
15    type: string;
16    required: boolean;
17    valueRequired: boolean;
18    positional: boolean;
19    choices: string[];
20    default: unknown;
21    help: string;
22  };
23  
24  /** Stable arg schema — every field is always present (no sparse objects). */
25  export function serializeArg(a: Arg): SerializedArg {
26    return {
27      name: a.name,
28      type: a.type ?? 'string',
29      required: !!a.required,
30      valueRequired: !!a.valueRequired,
31      positional: !!a.positional,
32      choices: a.choices ?? [],
33      default: a.default ?? null,
34      help: a.help ?? '',
35    };
36  }
37  
38  /** Full command metadata for structured output (json/yaml). */
39  export function serializeCommand(cmd: CliCommand) {
40    return {
41      command: fullName(cmd),
42      site: cmd.site,
43      name: cmd.name,
44      aliases: cmd.aliases ?? [],
45      description: cmd.description,
46      strategy: strategyLabel(cmd),
47      browser: !!cmd.browser,
48      args: cmd.args.map(serializeArg),
49      columns: cmd.columns ?? [],
50      domain: cmd.domain ?? null,
51      deprecated: cmd.deprecated ?? null,
52      replacedBy: cmd.replacedBy ?? null,
53    };
54  }
55  
56  // ── Formatting ──────────────────────────────────────────────────────────────
57  
58  /** Human-readable arg summary: `<required> [optional]` style. */
59  export function formatArgSummary(args: Arg[]): string {
60    return args
61      .map(a => {
62        if (a.positional) return a.required ? `<${a.name}>` : `[${a.name}]`;
63        return a.required ? `--${a.name}` : `[--${a.name}]`;
64      })
65      .join(' ');
66  }
67  
68  function summarizeChoices(choices: string[]): string {
69    if (choices.length <= 4) return choices.join(', ');
70    return `${choices.slice(0, 4).join(', ')}, ... (+${choices.length - 4} more)`;
71  }
72  
73  /** Generate the --help appendix showing registry metadata not exposed by Commander. */
74  export function formatRegistryHelpText(cmd: CliCommand): string {
75    const lines: string[] = [];
76    const choicesArgs = cmd.args.filter(a => a.choices?.length);
77    for (const a of choicesArgs) {
78      const prefix = a.positional ? `<${a.name}>` : `--${a.name}`;
79      const def = a.default != null ? `  (default: ${a.default})` : '';
80      lines.push(`  ${prefix}: ${summarizeChoices(a.choices!)}${def}`);
81    }
82    const meta: string[] = [];
83    meta.push(`Strategy: ${strategyLabel(cmd)}`);
84    meta.push(`Browser: ${cmd.browser ? 'yes' : 'no'}`);
85    if (cmd.domain) meta.push(`Domain: ${cmd.domain}`);
86    if (cmd.deprecated) meta.push(`Deprecated: ${typeof cmd.deprecated === 'string' ? cmd.deprecated : 'yes'}`);
87    if (cmd.replacedBy) meta.push(`Use instead: ${cmd.replacedBy}`);
88    if (cmd.aliases?.length) meta.push(`Aliases: ${cmd.aliases.join(', ')}`);
89    lines.push(meta.join(' | '));
90    if (cmd.columns?.length) lines.push(`Output columns: ${cmd.columns.join(', ')}`);
91    return '\n' + lines.join('\n') + '\n';
92  }