From 46bbd90fd73dd1775133c24b31fd6eb20bfa0380 Mon Sep 17 00:00:00 2001 From: Patrik Simek Date: Fri, 17 Apr 2026 11:57:22 +0200 Subject: [PATCH] feat: add command examples --- README.md | 6 ++ package-lock.json | 12 +-- package.json | 7 +- scripts/build-docs.ts | 185 ++++++++++++++++++++++++++++++++++++++++++ src/categories.ts | 52 ++++++------ src/commands.ts | 19 +++-- src/examples.ts | 106 ++++++++++++++++++++++++ test/commands.spec.ts | 5 +- test/examples.spec.ts | 179 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 527 insertions(+), 44 deletions(-) create mode 100644 scripts/build-docs.ts create mode 100644 src/examples.ts create mode 100644 test/examples.spec.ts diff --git a/README.md b/README.md index f7d98ab..9dc72eb 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,12 @@ brew install integromat/tap/make-cli npm install -g @makehq/cli ``` +Or run directly without installing: + +```bash +npx @makehq/cli scenarios list --team-id=123 +``` + ### Binary releases Pre-built binaries are available for download from the [GitHub Releases](https://github.com/integromat/make-cli/releases) page: diff --git a/package-lock.json b/package-lock.json index d99ba4e..59636c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@makehq/cli", - "version": "1.2.1", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@makehq/cli", - "version": "1.2.1", + "version": "1.3.0", "license": "MIT", "dependencies": { "@inquirer/prompts": "^8.3.2", - "@makehq/sdk": "^1.1.1", + "@makehq/sdk": "^1.2.1", "commander": "^14.0.3", "open": "^11.0.0" }, @@ -2043,9 +2043,9 @@ } }, "node_modules/@makehq/sdk": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@makehq/sdk/-/sdk-1.1.1.tgz", - "integrity": "sha512-C7SJ1OZyitMH03t25Uh/eGxHDZR+ZnaJAnm49CP4+8Ju9HqnwOkyVEYnk07YHE8YMmSqWSx7aNyhGh4W301TjQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@makehq/sdk/-/sdk-1.2.1.tgz", + "integrity": "sha512-VYuNmZqMWl0yxLdKa3w+pXhGGRL5p3io78BQxrw+kDNpns3AiXfRojjbqkEl2xaWZXdb0ShNLIdwPU8PElWC5g==", "license": "MIT", "engines": { "node": ">=20" diff --git a/package.json b/package.json index 7455f53..2843823 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@makehq/cli", - "version": "1.2.1", + "version": "1.3.0", "description": "A command-line tool for Make automation platform", "license": "MIT", "author": "Make", @@ -17,7 +17,8 @@ "build": "tsup", "test": "jest --coverage --coverageReporters=\"text\" --runInBand --forceExit --verbose false --testMatch \"**/test/**/*.spec.ts\"", "lint": "tsc && eslint --quiet .", - "format": "npx prettier . --write" + "format": "npx prettier . --write", + "build:docs": "npx tsx scripts/build-docs.ts" }, "engines": { "node": ">=20" @@ -27,7 +28,7 @@ ], "dependencies": { "@inquirer/prompts": "^8.3.2", - "@makehq/sdk": "^1.1.1", + "@makehq/sdk": "^1.2.1", "commander": "^14.0.3", "open": "^11.0.0" }, diff --git a/scripts/build-docs.ts b/scripts/build-docs.ts new file mode 100644 index 0000000..3907e20 --- /dev/null +++ b/scripts/build-docs.ts @@ -0,0 +1,185 @@ +import { writeFileSync, mkdirSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { MakeMCPTools } from '@makehq/sdk/mcp'; +import type { MakeMCPTool, JSONSchema } from '@makehq/sdk/mcp'; +import { CATEGORY_TITLES, CATEGORY_GROUPS } from '../src/categories.js'; +import { camelToKebab, formatExampleCommand } from '../src/examples.js'; +import { deriveActionName } from '../src/commands.js'; + +const DOCS_DIR = join(import.meta.dirname, '..', 'docs'); + +function schemaTypeLabel(schema: JSONSchema): string { + const type = Array.isArray(schema.type) ? schema.type[0] : schema.type; + if (type === 'array' && schema.items) { + const inner = Array.isArray(schema.items.type) ? schema.items.type[0] : schema.items.type; + return `${inner}[]`; + } + return type ?? 'string'; +} + +function buildToolSection(tool: MakeMCPTool, categorySlug: string): string { + const action = deriveActionName(tool.name, tool.category); + const lines: string[] = []; + + lines.push(`### \`make-cli ${categorySlug} ${action}\``); + lines.push(''); + lines.push(tool.description); + lines.push(''); + + const properties = tool.inputSchema.properties ?? {}; + const required = new Set(tool.inputSchema.required ?? []); + const propEntries = Object.entries(properties); + + if (propEntries.length > 0) { + lines.push('**Options**'); + lines.push(''); + lines.push('| Option | Description | Required |'); + lines.push('|--------|-------------|----------|'); + + for (const [propName, schema] of propEntries) { + const flagName = camelToKebab(propName); + const type = schemaTypeLabel(schema); + const isBooleanFlag = type === 'boolean'; + const flag = isBooleanFlag + ? schema.default === true + ? `--no-${flagName}` + : `--${flagName}` + : `--${flagName}`; + + const isRequired = required.has(propName) && !isBooleanFlag; + const propDesc = schema.description?.replace(/\|/g, '\\|').replace(/\n/g, ' ') ?? ''; + + lines.push(`| \`${flag}\` | ${propDesc} | ${isRequired ? 'Yes' : 'No'} |`); + } + + lines.push(''); + } + + lines.push('**Example**'); + lines.push(''); + lines.push('```bash'); + + const cmd = `make-cli ${categorySlug} ${action}`; + const example = tool.examples?.[0]; + if (example && Object.keys(example).length > 0) { + lines.push(formatExampleCommand(cmd, example)); + } else { + lines.push(cmd); + } + + lines.push('```'); + + return lines.join('\n'); +} + +function buildCategoryDoc(categorySlug: string, tools: MakeMCPTool[]): string { + const originalCategory = tools[0]!.category; + const title = CATEGORY_TITLES[originalCategory] ?? categorySlug; + const lines: string[] = []; + + lines.push(`## ${title}`); + lines.push(''); + lines.push(`Manage your ${title.toLowerCase()}.`); + lines.push(''); + lines.push('---'); + + for (let i = 0; i < tools.length; i++) { + lines.push(''); + lines.push(buildToolSection(tools[i]!, categorySlug)); + if (i < tools.length - 1) { + lines.push(''); + lines.push('---'); + } + } + + lines.push(''); + + return lines.join('\n'); +} + +function buildIndex(categoryMap: Map): string { + const lines: string[] = []; + + lines.push('# Make CLI Documentation'); + lines.push(''); + lines.push('Command-line tool for the [Make](https://www.make.com) automation platform.'); + lines.push(''); + lines.push('## Global Options'); + lines.push(''); + lines.push('| Flag | Description |'); + lines.push('| --- | --- |'); + lines.push('| `--api-key ` | Make API key (or set `MAKE_API_KEY` env var) |'); + lines.push('| `--zone ` | Make zone, e.g. `eu1.make.com` (or set `MAKE_ZONE` env var) |'); + lines.push('| `--output ` | Output format: `json` (default), `compact`, `table` |'); + lines.push('| `--version` | Show version number |'); + lines.push('| `--help` | Display help |'); + lines.push(''); + lines.push('## Authentication'); + lines.push(''); + lines.push('```bash'); + lines.push('# Interactive login (saves credentials locally)'); + lines.push('make-cli login'); + lines.push(''); + lines.push('# Check who you are logged in as'); + lines.push('make-cli whoami'); + lines.push(''); + lines.push('# Log out'); + lines.push('make-cli logout'); + lines.push('```'); + lines.push(''); + lines.push('## Command Reference'); + lines.push(''); + + const groups = new Map(); + + for (const [slug, tools] of categoryMap) { + const originalCategory = tools[0]!.category; + const title = CATEGORY_TITLES[originalCategory] ?? slug; + const group = CATEGORY_GROUPS[originalCategory] ?? 'Commands:'; + const groupName = group.replace(/:$/, ''); + + const entries = groups.get(groupName) ?? []; + entries.push({ slug, title }); + groups.set(groupName, entries); + } + + for (const [groupName, entries] of groups) { + lines.push(`### ${groupName}`); + lines.push(''); + lines.push('| Category | Description |'); + lines.push('| --- | --- |'); + for (const { slug, title } of entries) { + lines.push(`| [\`${slug}\`](./${slug}.md) | ${title} |`); + } + lines.push(''); + } + + return lines.join('\n'); +} + +// --- Main --- + +const categoryMap = new Map(); + +for (const tool of MakeMCPTools) { + const slug = tool.category.replace(/\./g, '-'); + const group = categoryMap.get(slug) ?? []; + group.push(tool); + categoryMap.set(slug, group); +} + +if (!existsSync(DOCS_DIR)) { + mkdirSync(DOCS_DIR, { recursive: true }); +} + +for (const [slug, tools] of categoryMap) { + const content = buildCategoryDoc(slug, tools); + writeFileSync(join(DOCS_DIR, `${slug}.md`), content); +} + +const index = buildIndex(categoryMap); +writeFileSync(join(DOCS_DIR, 'README.md'), index); + +const totalTools = MakeMCPTools.length; +const totalCategories = categoryMap.size; +console.log(`Generated docs for ${totalTools} commands across ${totalCategories} categories in docs/`); diff --git a/src/categories.ts b/src/categories.ts index bca2da5..73fdd21 100644 --- a/src/categories.ts +++ b/src/categories.ts @@ -14,45 +14,45 @@ export const CATEGORY_GROUPS: Record = { hooks: 'Scenarios:', devices: 'Scenarios:', 'data-structures': 'Scenarios:', - 'data-stores': 'Data Stores:', - 'data-store-records': 'Data Stores:', - teams: 'Account Management:', - organizations: 'Account Management:', - users: 'Account Management:', + 'data-stores': 'Data stores:', + 'data-store-records': 'Data stores:', + teams: 'Account management:', + organizations: 'Account management:', + users: 'Account management:', enums: 'Others:', // SDK categories - 'sdk-apps': 'Custom App Development:', - 'sdk-connections': 'Custom App Development:', - 'sdk-functions': 'Custom App Development:', - 'sdk-modules': 'Custom App Development:', - 'sdk-rpcs': 'Custom App Development:', - 'sdk-webhooks': 'Custom App Development:', + 'sdk-apps': 'Custom app development:', + 'sdk-connections': 'Custom app development:', + 'sdk-functions': 'Custom app development:', + 'sdk-modules': 'Custom app development:', + 'sdk-rpcs': 'Custom app development:', + 'sdk-webhooks': 'Custom app development:', }; export const CATEGORY_TITLES: Record = { // Top-level categories connections: 'Connections', - 'credential-requests': 'Credential Requests', - 'data-store-records': 'Data Store Records', - 'data-stores': 'Data Stores', - 'data-structures': 'Data Structures', - enums: 'Shared Enumerations', - executions: 'Scenario Executions', - folders: 'Scenario Folders', - functions: 'Custom Functions', + 'credential-requests': 'Credential requests', + 'data-store-records': 'Data store records', + 'data-stores': 'Data stores', + 'data-structures': 'Data structures', + enums: 'Shared enumerations', + executions: 'Scenario executions', + folders: 'Scenario folders', + functions: 'Custom functions', hooks: 'Webhooks', devices: 'Devices', - 'incomplete-executions': 'Incomplete Executions', + 'incomplete-executions': 'Incomplete executions', keys: 'Keys', organizations: 'Organizations', scenarios: 'Scenarios', teams: 'Teams', users: 'Users', // SDK categories - 'sdk-apps': 'App Definitions', - 'sdk-connections': 'App Connections', - 'sdk-functions': 'App Functions', - 'sdk-modules': 'App Modules', - 'sdk-rpcs': 'App Remote Procedures', - 'sdk-webhooks': 'App Webhooks', + 'sdk-apps': 'App definitions', + 'sdk-connections': 'App connections', + 'sdk-functions': 'App functions', + 'sdk-modules': 'App modules', + 'sdk-rpcs': 'App remote procedures', + 'sdk-webhooks': 'App webhooks', }; diff --git a/src/commands.ts b/src/commands.ts index b830a83..c5a0bb8 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -7,6 +7,7 @@ import { MakeError } from '@makehq/sdk'; import { resolveAuth } from './auth.js'; import { formatOutput, type OutputFormat } from './output.js'; import { CATEGORY_TITLES, CATEGORY_GROUPS } from './categories.js'; +import { camelToKebab, formatExampleCommand } from './examples.js'; /** * Derives the CLI action name from an MCP tool name and its category. @@ -25,13 +26,6 @@ export function deriveActionName(toolName: string, category: string): string { return toolName.slice(prefix.length).replace(/_/g, '-'); } -/** - * Converts a camelCase string to kebab-case. - */ -export function camelToKebab(str: string): string { - return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`); -} - /** * Converts a kebab-case string to camelCase. */ @@ -128,6 +122,17 @@ function registerToolAsCommand(parent: Command, tool: MakeMCPTool, category: str cmd.addOption(option); } + const example = tool.examples?.[0]; + if (example && Object.keys(example).length > 0) { + const slug = category.replace(/\./g, '-'); + const exampleCmd = formatExampleCommand(`make-cli ${slug} ${actionName}`, example); + const indented = exampleCmd + .split('\n') + .map(l => ' ' + l) + .join('\n'); + cmd.addHelpText('after', `\nExample:\n\n${indented}\n`); + } + cmd.action(async (localOptions: Record) => { const globalOptions = cmd.optsWithGlobals(); const { token, zone } = await resolveAuth({ diff --git a/src/examples.ts b/src/examples.ts new file mode 100644 index 0000000..48581a8 --- /dev/null +++ b/src/examples.ts @@ -0,0 +1,106 @@ +import type { JSONValue } from '@makehq/sdk'; + +/** + * Converts a camelCase string to kebab-case. + */ +export function camelToKebab(str: string): string { + return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`); +} + +/** + * Formats a single example value for shell display. + * Returns both a flat (single-line) and an expanded (possibly multi-line) representation. + * JSON strings longer than 60 characters are pretty-printed in the expanded form. + */ +export function formatExampleValue(value: JSONValue): { flat: string; expanded: string } { + if (typeof value === 'number') { + const s = String(value); + return { flat: s, expanded: s }; + } + + if (typeof value === 'string') { + let parsed: unknown; + try { + parsed = JSON.parse(value); + } catch { + /* not JSON */ + } + + if (typeof parsed === 'object' && parsed !== null) { + const compact = JSON.stringify(parsed); + const flat = `'${compact}'`; + if (compact.length <= 60) { + return { flat, expanded: flat }; + } + const pretty = JSON.stringify(parsed, null, 2); + const prettyLines = pretty.split('\n'); + const indented = prettyLines.map((l, i) => (i === 0 ? l : ' ' + l)); + return { flat, expanded: `'${indented.join('\n')}'` }; + } + + if (/[\s'"]/.test(value)) { + const q = `'${value}'`; + return { flat: q, expanded: q }; + } + return { flat: value, expanded: value }; + } + + if (typeof value === 'object' && value !== null) { + const compact = JSON.stringify(value); + const flat = `'${compact}'`; + if (compact.length <= 60) { + return { flat, expanded: flat }; + } + const pretty = JSON.stringify(value, null, 2); + const prettyLines = pretty.split('\n'); + const indented = prettyLines.map((l, i) => (i === 0 ? l : ' ' + l)); + return { flat, expanded: `'${indented.join('\n')}'` }; + } + + const s = String(value); + return { flat: s, expanded: s }; +} + +/** + * Builds a formatted shell example command from a base command and an example payload. + * Uses single-line format when the result fits within 80 characters, + * otherwise switches to multi-line with backslash continuations. + */ +export function formatExampleCommand(command: string, example: Record): string { + const entries = Object.entries(example).filter(([, v]) => v !== false); + if (entries.length === 0) return command; + + const args: { flat: string; expanded: string }[] = []; + for (const [name, value] of entries) { + const flag = `--${camelToKebab(name)}`; + if (typeof value === 'boolean') { + args.push({ flat: flag, expanded: flag }); + } else { + const formatted = formatExampleValue(value); + args.push({ + flat: `${flag}=${formatted.flat}`, + expanded: `${flag}=${formatted.expanded}`, + }); + } + } + + const singleLine = `${command} ${args.map(a => a.flat).join(' ')}`; + const hasMultiLine = args.some(a => a.expanded.includes('\n')); + + if (!hasMultiLine && singleLine.length <= 80) { + return singleLine; + } + + const blocks = [command, ...args.map(a => ` ${a.expanded}`)]; + const parts: string[] = []; + for (let i = 0; i < blocks.length; i++) { + if (i < blocks.length - 1) { + const blockLines = blocks[i]!.split('\n'); + blockLines[blockLines.length - 1] += ' \\'; + parts.push(blockLines.join('\n')); + } else { + parts.push(blocks[i]!); + } + } + return parts.join('\n'); +} diff --git a/test/commands.spec.ts b/test/commands.spec.ts index 7175ad0..6d7729d 100644 --- a/test/commands.spec.ts +++ b/test/commands.spec.ts @@ -1,6 +1,7 @@ import { describe, expect, it, beforeEach, afterEach, jest } from '@jest/globals'; import { Command, Option } from 'commander'; -import { deriveActionName, camelToKebab, coerceValue, buildCommands } from '../src/commands.js'; +import { deriveActionName, coerceValue, buildCommands } from '../src/commands.js'; +import { camelToKebab } from '../src/examples.js'; import { resolveAuth } from '../src/auth.js'; import { formatOutput } from '../src/output.js'; import type { MakeMCPTool } from '@makehq/sdk/mcp'; @@ -200,7 +201,7 @@ describe('CLI: buildCommands', () => { ]); const sdkApps = program.commands.find(c => c.name() === 'sdk-apps'); - expect((sdkApps as any).helpGroup()).toBe('Custom App Development:'); + expect((sdkApps as any).helpGroup()).toBe('Custom app development:'); }); it('should register required options with syntax', () => { diff --git a/test/examples.spec.ts b/test/examples.spec.ts new file mode 100644 index 0000000..855e017 --- /dev/null +++ b/test/examples.spec.ts @@ -0,0 +1,179 @@ +import { describe, expect, it } from '@jest/globals'; +import { formatExampleValue, formatExampleCommand, camelToKebab } from '../src/examples.js'; + +describe('camelToKebab', () => { + it('should convert camelCase to kebab-case', () => { + expect(camelToKebab('teamId')).toBe('team-id'); + expect(camelToKebab('scenarioId')).toBe('scenario-id'); + expect(camelToKebab('dataStoreId')).toBe('data-store-id'); + }); + + it('should leave single words unchanged', () => { + expect(camelToKebab('name')).toBe('name'); + expect(camelToKebab('status')).toBe('status'); + }); +}); + +describe('formatExampleValue', () => { + it('should format numbers', () => { + expect(formatExampleValue(5)).toEqual({ flat: '5', expanded: '5' }); + expect(formatExampleValue(3.14)).toEqual({ flat: '3.14', expanded: '3.14' }); + }); + + it('should format plain strings', () => { + expect(formatExampleValue('my-app')).toEqual({ flat: 'my-app', expanded: 'my-app' }); + }); + + it('should quote strings with spaces', () => { + expect(formatExampleValue('hello world')).toEqual({ + flat: "'hello world'", + expanded: "'hello world'", + }); + }); + + it('should quote strings with quotes', () => { + expect(formatExampleValue('say "hi"')).toEqual({ + flat: `'say "hi"'`, + expanded: `'say "hi"'`, + }); + }); + + it('should format short JSON strings as compact single-quoted', () => { + const result = formatExampleValue('{"type":"on-demand"}'); + expect(result.flat).toBe(`'{"type":"on-demand"}'`); + expect(result.expanded).toBe(result.flat); + }); + + it('should expand long JSON strings into pretty-printed multi-line', () => { + const longJson = JSON.stringify({ + name: 'Test Scenario', + flow: [{ id: 1, module: 'google-email:watchEmails', version: 1, parameters: { connection: 5 } }], + }); + const result = formatExampleValue(longJson); + + expect(result.flat).toBe(`'${longJson}'`); + expect(result.expanded).toContain('\n'); + expect(result.expanded).toMatch(/^'\{/); + expect(result.expanded).toMatch(/\}'$/); + expect(result.expanded).toContain(' "name": "Test Scenario"'); + }); + + it('should indent multi-line JSON with 2 extra spaces per level', () => { + const obj = { + name: 'A long enough name to push this past the sixty character limit', + nested: { key: 'value' }, + }; + const json = JSON.stringify(obj); + expect(json.length).toBeGreaterThan(60); + + const result = formatExampleValue(json); + const lines = result.expanded.split('\n'); + // First line starts with '{ + expect(lines[0]).toBe("'{"); + // Property lines get 2 extra spaces (JSON indent 2 + extra 2 = 4) + expect(lines[1]).toMatch(/^ {4}"/); + // Last line is closing }' + expect(lines[lines.length - 1]).toMatch(/^ {2}\}'$/); + }); + + it('should format short direct objects as compact', () => { + const result = formatExampleValue({ key: 'value' }); + expect(result.flat).toBe(`'{"key":"value"}'`); + expect(result.expanded).toBe(result.flat); + }); + + it('should expand long direct objects into multi-line', () => { + const obj = { + input: [{ name: 'myInput', type: 'text', required: true }], + output: [{ name: 'myOutput', type: 'text' }], + extra: 'padding to make this object long enough to exceed the threshold', + }; + const result = formatExampleValue(obj); + expect(result.expanded).toContain('\n'); + }); + + it('should handle null and undefined', () => { + expect(formatExampleValue(null)).toEqual({ flat: 'null', expanded: 'null' }); + expect(formatExampleValue(undefined)).toEqual({ flat: 'undefined', expanded: 'undefined' }); + }); +}); + +describe('formatExampleCommand', () => { + it('should return just the command for empty examples', () => { + expect(formatExampleCommand('make-cli apps list', {})).toBe('make-cli apps list'); + }); + + it('should format a single numeric arg on one line', () => { + const result = formatExampleCommand('make-cli scenarios list', { teamId: 5 }); + expect(result).toBe('make-cli scenarios list --team-id=5'); + }); + + it('should format multiple short args on one line', () => { + const result = formatExampleCommand('make-cli apps get', { name: 'my-app', version: 1 }); + expect(result).toBe('make-cli apps get --name=my-app --version=1'); + }); + + it('should format boolean true as bare flag', () => { + const result = formatExampleCommand('make-cli scenarios run', { + scenarioId: 925, + responsive: true, + }); + expect(result).toBe('make-cli scenarios run --scenario-id=925 --responsive'); + }); + + it('should skip boolean false values', () => { + const result = formatExampleCommand('make-cli scenarios run', { + scenarioId: 925, + responsive: false, + }); + expect(result).toBe('make-cli scenarios run --scenario-id=925'); + }); + + it('should switch to multi-line when exceeding 80 characters', () => { + const result = formatExampleCommand('make-cli scenarios update', { + scenarioId: 925, + name: 'Updated Scenario', + scheduling: '{"type":"indefinitely","interval":900}', + }); + expect(result).toContain(' \\\n'); + expect(result).toMatch(/^make-cli scenarios update \\/); + expect(result).toContain(' --scenario-id=925'); + }); + + it('should switch to multi-line when values contain newlines', () => { + const longJson = JSON.stringify({ + name: 'Gmail Attachments to Google Drive', + flow: [{ id: 1, module: 'google-email:watchEmails', version: 1, parameters: { connection: 5 } }], + }); + const result = formatExampleCommand('make-cli scenarios create', { + teamId: 5, + blueprint: longJson, + }); + expect(result).toContain(' \\\n'); + expect(result).toContain(" --blueprint='{"); + }); + + it('should place backslash continuations on the last line of multi-line args', () => { + const longJson = JSON.stringify({ key: 'x'.repeat(80) }); + const result = formatExampleCommand('make-cli test cmd', { + first: longJson, + second: 42, + }); + const lines = result.split('\n'); + // The closing }' of the first arg should have ' \' appended + const closingLine = lines.find(l => l.includes("}'")); + expect(closingLine).toMatch(/\\$/); + // The last line should NOT have a backslash + expect(lines[lines.length - 1]).not.toMatch(/\\$/); + }); + + it('should not add backslash after the last argument', () => { + const result = formatExampleCommand('make-cli scenarios update', { + scenarioId: 925, + name: 'Updated Scenario', + scheduling: '{"type":"indefinitely","interval":900}', + }); + const lines = result.split('\n'); + expect(lines[lines.length - 1]).not.toMatch(/\\$/); + }); +});