sharetribe-cli 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Custom help formatter to match flex-cli output exactly
3
+ */
4
+
5
+ import { Command, Help } from 'commander';
6
+ import chalk from 'chalk';
7
+
8
+ /**
9
+ * Formats help text to match flex-cli style
10
+ *
11
+ * flex-cli format:
12
+ * - Description (no label)
13
+ * - VERSION section (for main help only)
14
+ * - USAGE section
15
+ * - COMMANDS section (flattened list of all leaf commands)
16
+ * - OPTIONS section (for subcommands only, not main)
17
+ * - Subcommand help instructions
18
+ *
19
+ * @param cmd - Commander command instance
20
+ * @returns Formatted help text matching flex-cli
21
+ */
22
+ export function formatHelp(cmd: Command): string {
23
+ const parts: string[] = [];
24
+ const isRootCommand = !cmd.parent;
25
+
26
+ // Description (no label, just the text)
27
+ const description = cmd.description();
28
+ if (description) {
29
+ parts.push(description);
30
+ parts.push('');
31
+ }
32
+
33
+ // VERSION section (only for root command)
34
+ if (isRootCommand) {
35
+ const version = cmd.version();
36
+ if (version) {
37
+ parts.push('VERSION');
38
+ parts.push(` ${version}`);
39
+ parts.push('');
40
+ }
41
+ }
42
+
43
+ // USAGE section
44
+ parts.push('USAGE');
45
+ const usage = formatUsage(cmd);
46
+ parts.push(` $ ${usage}`);
47
+ parts.push('');
48
+
49
+ // COMMANDS section
50
+ // Note: If command has an action (options), don't show COMMANDS section (like flex-cli)
51
+ const allCommands = collectAllLeafCommands(cmd);
52
+ const hasAction = cmd.options.length > 0 && !isRootCommand;
53
+
54
+ if (allCommands.length > 0 && !hasAction) {
55
+ parts.push('COMMANDS');
56
+
57
+ // Calculate max command name length for alignment
58
+ const maxLength = Math.max(...allCommands.map(c => c.name.length));
59
+
60
+ for (const cmdInfo of allCommands) {
61
+ const paddedName = cmdInfo.name.padEnd(maxLength + 2);
62
+ parts.push(` ${paddedName}${cmdInfo.description}`);
63
+ }
64
+ parts.push('');
65
+ }
66
+
67
+ // OPTIONS section (only for subcommands, not root)
68
+ if (!isRootCommand) {
69
+ const options = cmd.options;
70
+ if (options.length > 0) {
71
+ parts.push('OPTIONS');
72
+
73
+ // Calculate max option flags length for alignment
74
+ const maxFlagsLength = Math.max(...options.map(opt => formatOptionFlags(opt).length));
75
+
76
+ for (const opt of options) {
77
+ const flags = formatOptionFlags(opt);
78
+ const paddedFlags = flags.padEnd(maxFlagsLength + 2);
79
+ const desc = opt.description || '';
80
+ parts.push(` ${paddedFlags}${desc}`);
81
+ }
82
+ parts.push('');
83
+ }
84
+ }
85
+
86
+ // Subcommand help instructions (only for main and group commands without actions)
87
+ if (allCommands.length > 0 && !hasAction) {
88
+ parts.push('Subcommand help:');
89
+ const cmdName = getCommandName(cmd);
90
+ parts.push(` $ ${cmdName} help [COMMAND]`);
91
+ }
92
+
93
+ // Always add empty line at end to match flex-cli
94
+ parts.push('');
95
+
96
+ return parts.join('\n');
97
+ }
98
+
99
+ /**
100
+ * Recursively collects all commands (both parent and leaf commands)
101
+ *
102
+ * flex-cli shows ALL commands, including parent commands that have their own actions
103
+ * Example: both "events" and "events tail" are shown
104
+ *
105
+ * @param cmd - Commander command instance
106
+ * @returns Array of command info objects with name and description
107
+ */
108
+ function collectAllLeafCommands(cmd: Command): Array<{ name: string; description: string }> {
109
+ const results: Array<{ name: string; description: string }> = [];
110
+ const commands = cmd.commands.filter(c => !c._hidden && c.name() !== 'help');
111
+
112
+ for (const subCmd of commands) {
113
+ const fullName = getCommandFullName(subCmd);
114
+ const subCommands = subCmd.commands.filter(c => !c._hidden);
115
+
116
+ // Add this command if it has an action or description
117
+ if (subCmd.description()) {
118
+ results.push({
119
+ name: fullName,
120
+ description: subCmd.description() || ''
121
+ });
122
+ }
123
+
124
+ // If it has subcommands, recurse and add those too
125
+ if (subCommands.length > 0) {
126
+ const subResults = collectAllLeafCommands(subCmd);
127
+ for (const sub of subResults) {
128
+ results.push(sub);
129
+ }
130
+ }
131
+ }
132
+
133
+ // Add "help" command at the beginning if this is root
134
+ if (!cmd.parent) {
135
+ results.unshift({
136
+ name: 'help',
137
+ description: 'display help for Flex CLI'
138
+ });
139
+ }
140
+
141
+ // Sort alphabetically by command name
142
+ results.sort((a, b) => a.name.localeCompare(b.name));
143
+
144
+ return results;
145
+ }
146
+
147
+ /**
148
+ * Gets the command name for usage (flex-cli vs sharetribe-cli)
149
+ *
150
+ * @param cmd - Commander command instance
151
+ * @returns Command name (e.g., "sharetribe-cli" or "sharetribe-cli process")
152
+ */
153
+ function getCommandName(cmd: Command): string {
154
+ const names: string[] = [];
155
+ let current: Command | null = cmd;
156
+
157
+ while (current) {
158
+ if (current.name()) {
159
+ names.unshift(current.name());
160
+ }
161
+ current = current.parent;
162
+ }
163
+
164
+ // Replace first name with "sharetribe-cli" (or "flex-cli" for reference)
165
+ if (names.length > 0) {
166
+ names[0] = 'sharetribe-cli';
167
+ }
168
+
169
+ return names.join(' ');
170
+ }
171
+
172
+ /**
173
+ * Formats the USAGE line
174
+ *
175
+ * @param cmd - Commander command instance
176
+ * @returns Usage string (e.g., "sharetribe-cli [COMMAND]" or "sharetribe-cli process list")
177
+ */
178
+ function formatUsage(cmd: Command): string {
179
+ const cmdName = getCommandName(cmd);
180
+ const commands = cmd.commands.filter(c => !c._hidden);
181
+ const hasOptions = cmd.options.length > 0;
182
+ const isRoot = !cmd.parent;
183
+
184
+ // Root command always shows [COMMAND] if it has subcommands
185
+ if (isRoot && commands.length > 0) {
186
+ return `${cmdName} [COMMAND]`;
187
+ }
188
+
189
+ // If command has options (its own action), don't show [COMMAND] even if it has subcommands
190
+ // This matches flex-cli behavior for commands like "process" which have both action and subcommands
191
+ if (commands.length > 0 && !hasOptions) {
192
+ // Has subcommands but no action
193
+ return `${cmdName} [COMMAND]`;
194
+ } else {
195
+ // Leaf command or command with action - just show the command path
196
+ return cmdName;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Gets the full command name including parent path
202
+ *
203
+ * @param cmd - Commander command instance
204
+ * @returns Full command name (e.g., "process list" or "events tail")
205
+ */
206
+ function getCommandFullName(cmd: Command): string {
207
+ const names: string[] = [];
208
+ let current: Command | null = cmd;
209
+
210
+ while (current && current.parent) {
211
+ if (current.name()) {
212
+ names.unshift(current.name());
213
+ }
214
+ current = current.parent;
215
+ }
216
+
217
+ return names.join(' ');
218
+ }
219
+
220
+ /**
221
+ * Formats option flags for display
222
+ *
223
+ * @param opt - Commander option instance
224
+ * @returns Formatted flags string (e.g., "-m, --marketplace=MARKETPLACE_ID")
225
+ */
226
+ function formatOptionFlags(opt: any): string {
227
+ // Commander option flags are in opt.flags (e.g., "-m, --marketplace <MARKETPLACE_ID>")
228
+ // We need to parse and reformat this to match flex-cli style
229
+ const flagsStr = opt.flags || '';
230
+
231
+ // Parse the flags string
232
+ // Format: "-m, --marketplace <VALUE>" or "--flag" or "-f"
233
+ const parts = flagsStr.split(/,\s*/);
234
+ const formatted: string[] = [];
235
+
236
+ for (const part of parts) {
237
+ const trimmed = part.trim();
238
+
239
+ // Check if it has a value placeholder (angle brackets or square brackets)
240
+ const valueMatch = trimmed.match(/^((?:-{1,2}[\w-]+))\s*[<\[]([^\]>]+)[\]>]/);
241
+ if (valueMatch) {
242
+ // Has a value: "-m <MARKETPLACE_ID>" or "--marketplace <MARKETPLACE_ID>"
243
+ const flag = valueMatch[1];
244
+ const valueName = valueMatch[2];
245
+ formatted.push(`${flag}=${valueName}`);
246
+ } else {
247
+ // No value: just the flag
248
+ formatted.push(trimmed);
249
+ }
250
+ }
251
+
252
+ return formatted.join(', ');
253
+ }
254
+
255
+ /**
256
+ * Configures Commander.js to use custom help formatter
257
+ *
258
+ * @param program - Commander program instance
259
+ */
260
+ export function configureHelp(program: Command): void {
261
+ program.configureHelp({
262
+ formatHelp: (cmd: Command, helper: Help) => {
263
+ return formatHelp(cmd);
264
+ }
265
+ });
266
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Output formatting utilities
3
+ *
4
+ * Must match flex-cli output format exactly
5
+ */
6
+
7
+ import chalk from 'chalk';
8
+
9
+ /**
10
+ * Prints a table with headers and rows
11
+ *
12
+ * Matches flex-cli table formatting exactly
13
+ */
14
+ export function printTable(headers: string[], rows: Array<Record<string, string>>): void {
15
+ if (rows.length === 0) {
16
+ return;
17
+ }
18
+
19
+ // Calculate column widths
20
+ // flex-cli uses keywords (e.g., :version) which when stringified include the ':' prefix
21
+ // To match flex-cli widths, we add 1 to header length to simulate the ':' prefix
22
+ const widths: Record<string, number> = {};
23
+ for (const header of headers) {
24
+ widths[header] = header.length + 1; // +1 to match flex-cli keyword string behavior
25
+ }
26
+
27
+ for (const row of rows) {
28
+ for (const header of headers) {
29
+ const value = row[header] || '';
30
+ widths[header] = Math.max(widths[header] || 0, value.length);
31
+ }
32
+ }
33
+
34
+ // Print empty line before table (like flex-cli)
35
+ console.log('');
36
+
37
+ // Print header with bold formatting
38
+ // flex-cli format: each column padded to (max_width + 1), with single space separator between columns
39
+ // Last column: padding but no separator (interpose doesn't add separator after last element)
40
+ const headerParts = headers.map((h, i) => {
41
+ const width = widths[h] || 0;
42
+ const padded = h.padEnd(width + 1);
43
+ return i === headers.length - 1 ? padded : padded + ' ';
44
+ });
45
+ const headerRow = headerParts.join('');
46
+ console.log(chalk.bold.black(headerRow));
47
+
48
+ // Print rows with same formatting
49
+ for (const row of rows) {
50
+ const rowParts = headers.map((h, i) => {
51
+ const value = row[h] || '';
52
+ const width = widths[h] || 0;
53
+ const padded = value.padEnd(width + 1);
54
+ return i === headers.length - 1 ? padded : padded + ' ';
55
+ });
56
+ const rowStr = rowParts.join('');
57
+ console.log(rowStr);
58
+ }
59
+
60
+ // Print empty line after table (like flex-cli)
61
+ console.log('');
62
+ }
63
+
64
+ /**
65
+ * Prints an error message
66
+ */
67
+ export function printError(message: string): void {
68
+ console.error(chalk.red(`Error: ${message}`));
69
+ }
70
+
71
+ /**
72
+ * Prints a success message
73
+ */
74
+ export function printSuccess(message: string): void {
75
+ console.log(chalk.green(message));
76
+ }
77
+
78
+ /**
79
+ * Prints a warning message
80
+ */
81
+ export function printWarning(message: string): void {
82
+ console.log(chalk.yellow(`Warning: ${message}`));
83
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Comprehensive help output comparison tests
3
+ *
4
+ * Tests that help output matches flex-cli for all commands
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import { execSync } from 'child_process';
9
+
10
+ /**
11
+ * Executes a CLI command and returns output
12
+ */
13
+ function runCli(command: string, cli: 'flex' | 'sharetribe'): string {
14
+ const cliName = cli === 'flex' ? 'flex-cli' : 'sharetribe-cli';
15
+ try {
16
+ return execSync(`${cliName} ${command}`, {
17
+ encoding: 'utf-8',
18
+ stdio: ['pipe', 'pipe', 'pipe'],
19
+ });
20
+ } catch (error) {
21
+ if (error instanceof Error && 'stdout' in error && 'stderr' in error) {
22
+ const stdout = (error as any).stdout || '';
23
+ const stderr = (error as any).stderr || '';
24
+ return stdout + stderr;
25
+ }
26
+ throw error;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Normalizes help output for comparison (removes CLI name differences)
32
+ */
33
+ function normalizeHelp(output: string, cliName: string): string {
34
+ return output
35
+ .replace(new RegExp(cliName, 'g'), 'CLI')
36
+ .replace(/\s+$/gm, ''); // Trim trailing spaces per line
37
+ }
38
+
39
+ /**
40
+ * Compares help structure (sections present, not exact content)
41
+ */
42
+ function compareHelpStructure(flexOutput: string, shareOutput: string, cmdName: string) {
43
+ // Both should have description
44
+ const flexLines = flexOutput.split('\n');
45
+ const shareLines = shareOutput.split('\n');
46
+
47
+ // First line should be description
48
+ expect(shareLines[0]).toBeTruthy();
49
+ expect(shareLines[0]).not.toMatch(/^USAGE|^OPTIONS|^COMMANDS/);
50
+
51
+ // Should have USAGE section
52
+ expect(shareOutput).toContain('USAGE');
53
+
54
+ // Check if flex has OPTIONS
55
+ if (flexOutput.includes('OPTIONS')) {
56
+ expect(shareOutput).toContain('OPTIONS');
57
+ }
58
+
59
+ // Check if flex has COMMANDS
60
+ if (flexOutput.includes('COMMANDS')) {
61
+ expect(shareOutput).toContain('COMMANDS');
62
+ }
63
+ }
64
+
65
+ describe('Help Comparison Tests', () => {
66
+ describe('Main help', () => {
67
+ it('has same structure as flex-cli', () => {
68
+ const flexOutput = runCli('--help', 'flex');
69
+ const shareOutput = runCli('--help', 'sharetribe');
70
+
71
+ expect(shareOutput).toContain('VERSION');
72
+ expect(shareOutput).toContain('USAGE');
73
+ expect(shareOutput).toContain('COMMANDS');
74
+ expect(shareOutput).toContain('Subcommand help:');
75
+
76
+ // Should NOT have OPTIONS in main help
77
+ const lines = shareOutput.split('\n');
78
+ const commandsIndex = lines.findIndex(l => l === 'COMMANDS');
79
+ const subcommandIndex = lines.findIndex(l => l.startsWith('Subcommand help:'));
80
+ const betweenLines = lines.slice(commandsIndex, subcommandIndex);
81
+ expect(betweenLines.some(l => l === 'OPTIONS')).toBe(false);
82
+ });
83
+
84
+ it('commands are alphabetically sorted', () => {
85
+ const shareOutput = runCli('--help', 'sharetribe');
86
+ const lines = shareOutput.split('\n');
87
+ const commandsStartIndex = lines.findIndex(l => l === 'COMMANDS');
88
+ const commandLines = lines.slice(commandsStartIndex + 1).filter(l => l.match(/^\s+\w/));
89
+
90
+ const commandNames = commandLines.map(l => l.trim().split(/\s+/)[0]);
91
+ const sortedNames = [...commandNames].sort();
92
+
93
+ expect(commandNames).toEqual(sortedNames);
94
+ });
95
+
96
+ it('ends with empty line', () => {
97
+ const shareOutput = runCli('--help', 'sharetribe');
98
+ expect(shareOutput).toMatch(/\n$/);
99
+ expect(shareOutput).toMatch(/\n\n$/);
100
+ });
101
+ });
102
+
103
+ describe('help process', () => {
104
+ it('matches flex-cli structure', () => {
105
+ const flexOutput = runCli('help process', 'flex');
106
+ const shareOutput = runCli('help process', 'sharetribe');
107
+
108
+ compareHelpStructure(flexOutput, shareOutput, 'process');
109
+
110
+ // Should have OPTIONS (process has --path and --transition options)
111
+ expect(shareOutput).toContain('OPTIONS');
112
+ expect(shareOutput).toContain('--path');
113
+ expect(shareOutput).toContain('--transition');
114
+ });
115
+
116
+ it('has correct description', () => {
117
+ const shareOutput = runCli('help process', 'sharetribe');
118
+ const lines = shareOutput.split('\n');
119
+ expect(lines[0]).toBe('describe a process file');
120
+ });
121
+
122
+ it('has correct usage', () => {
123
+ const shareOutput = runCli('help process', 'sharetribe');
124
+ expect(shareOutput).toMatch(/\$ sharetribe-cli process$/m);
125
+ });
126
+ });
127
+
128
+ describe('help process list', () => {
129
+ it('matches flex-cli structure', () => {
130
+ const flexOutput = runCli('help process list', 'flex');
131
+ const shareOutput = runCli('help process list', 'sharetribe');
132
+
133
+ compareHelpStructure(flexOutput, shareOutput, 'process list');
134
+
135
+ expect(shareOutput).toContain('OPTIONS');
136
+ expect(shareOutput).toContain('--process');
137
+ expect(shareOutput).toContain('--marketplace');
138
+ });
139
+
140
+ it('has correct description', () => {
141
+ const shareOutput = runCli('help process list', 'sharetribe');
142
+ expect(shareOutput).toMatch(/^list all transaction processes/);
143
+ });
144
+ });
145
+
146
+ describe('help events', () => {
147
+ it('matches flex-cli structure', () => {
148
+ const flexOutput = runCli('help events', 'flex');
149
+ const shareOutput = runCli('help events', 'sharetribe');
150
+
151
+ compareHelpStructure(flexOutput, shareOutput, 'events');
152
+
153
+ expect(shareOutput).toContain('OPTIONS');
154
+ });
155
+
156
+ it('has correct description', () => {
157
+ const shareOutput = runCli('help events', 'sharetribe');
158
+ expect(shareOutput).toMatch(/^Get a list of events\./);
159
+ });
160
+ });
161
+
162
+ describe('help search', () => {
163
+ it('matches flex-cli structure', () => {
164
+ const flexOutput = runCli('help search', 'flex');
165
+ const shareOutput = runCli('help search', 'sharetribe');
166
+
167
+ compareHelpStructure(flexOutput, shareOutput, 'search');
168
+ });
169
+
170
+ it('has correct description', () => {
171
+ const shareOutput = runCli('help search', 'sharetribe');
172
+ expect(shareOutput).toMatch(/^list all search schemas/);
173
+ });
174
+ });
175
+
176
+ describe('help notifications', () => {
177
+ it('has correct structure', () => {
178
+ const shareOutput = runCli('help notifications', 'sharetribe');
179
+
180
+ expect(shareOutput).toContain('USAGE');
181
+ expect(shareOutput).toContain('COMMANDS');
182
+ });
183
+ });
184
+
185
+ describe('help login', () => {
186
+ it('has correct structure', () => {
187
+ const shareOutput = runCli('help login', 'sharetribe');
188
+
189
+ expect(shareOutput).toContain('USAGE');
190
+ expect(shareOutput).toContain('$ sharetribe-cli login');
191
+ });
192
+ });
193
+
194
+ describe('help logout', () => {
195
+ it('has correct structure', () => {
196
+ const shareOutput = runCli('help logout', 'sharetribe');
197
+
198
+ expect(shareOutput).toContain('USAGE');
199
+ expect(shareOutput).toContain('$ sharetribe-cli logout');
200
+ });
201
+ });
202
+
203
+ describe('help version', () => {
204
+ it('has correct structure', () => {
205
+ const shareOutput = runCli('help version', 'sharetribe');
206
+
207
+ expect(shareOutput).toContain('USAGE');
208
+ expect(shareOutput).toContain('$ sharetribe-cli version');
209
+ });
210
+ });
211
+
212
+ describe('All help commands have consistent format', () => {
213
+ const commands = [
214
+ 'help',
215
+ 'events',
216
+ 'events tail',
217
+ 'login',
218
+ 'logout',
219
+ 'process',
220
+ 'process list',
221
+ 'process create',
222
+ 'process push',
223
+ 'process pull',
224
+ 'process create-alias',
225
+ 'process update-alias',
226
+ 'process delete-alias',
227
+ 'search',
228
+ 'search set',
229
+ 'search unset',
230
+ 'notifications',
231
+ 'notifications preview',
232
+ 'notifications send',
233
+ 'stripe',
234
+ 'stripe update-version',
235
+ 'version',
236
+ ];
237
+
238
+ commands.forEach(cmd => {
239
+ it(`help ${cmd} - has description and USAGE`, () => {
240
+ const output = runCli(`help ${cmd}`, 'sharetribe');
241
+ const lines = output.split('\n').filter(l => l.trim());
242
+
243
+ // Should have at least description and USAGE
244
+ expect(lines.length).toBeGreaterThan(2);
245
+ expect(output).toContain('USAGE');
246
+ expect(output).toMatch(/\$ sharetribe-cli/);
247
+ });
248
+
249
+ it(`help ${cmd} - ends with empty line`, () => {
250
+ const output = runCli(`help ${cmd}`, 'sharetribe');
251
+ expect(output).toMatch(/\n\n$/);
252
+ });
253
+ });
254
+ });
255
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Tests for process commands
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { parseProcessFile } from 'sharetribe-flex-build-sdk';
7
+
8
+ describe('edn-process', () => {
9
+ it('should parse process definition structure', () => {
10
+ // This would test actual EDN parsing
11
+ // For now, verify the function exists and has correct signature
12
+ expect(typeof parseProcessFile).toBe('function');
13
+ });
14
+ });