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.
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "sharetribe-cli",
3
+ "version": "1.15.0",
4
+ "description": "Unofficial Sharetribe CLI - 100% compatible with flex-cli",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "sharetribe-cli": "./dist/index.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18.0.0"
12
+ },
13
+ "scripts": {
14
+ "build": "node build.js",
15
+ "build:dev": "node build.js --dev",
16
+ "dev": "node build.js --dev --watch",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
19
+ "lint": "eslint src/**/*.ts",
20
+ "format": "prettier --write \"src/**/*.ts\"",
21
+ "format:check": "prettier --check \"src/**/*.ts\"",
22
+ "typecheck": "tsc --noEmit",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "keywords": [
26
+ "sharetribe",
27
+ "flex",
28
+ "cli",
29
+ "marketplace",
30
+ "transaction-process"
31
+ ],
32
+ "author": "",
33
+ "license": "Apache-2.0",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/jayenashar/sharetribe-cli"
37
+ },
38
+ "dependencies": {
39
+ "@types/yargs": "^17.0.35",
40
+ "chalk": "^5.3.0",
41
+ "commander": "^12.1.0",
42
+ "inquirer": "^9.2.23",
43
+ "jsedn": "^0.4.1",
44
+ "sharetribe-flex-build-sdk": "1.15.0",
45
+ "yargs": "^18.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/inquirer": "^9.0.7",
49
+ "@types/node": "^20.17.10",
50
+ "@typescript-eslint/eslint-plugin": "^8.18.2",
51
+ "@typescript-eslint/parser": "^8.18.2",
52
+ "esbuild": "^0.27.2",
53
+ "eslint": "^9.17.0",
54
+ "prettier": "^3.4.2",
55
+ "typescript": "^5.7.2",
56
+ "vitest": "^4.0.16"
57
+ }
58
+ }
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Assets commands - manage marketplace assets
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import {
7
+ pullAssets as sdkPullAssets,
8
+ pushAssets as sdkPushAssets,
9
+ } from 'sharetribe-flex-build-sdk';
10
+ import { printError } from '../../util/output.js';
11
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs';
12
+ import { join, dirname } from 'node:path';
13
+ import { createHash } from 'node:crypto';
14
+ import edn from 'jsedn';
15
+
16
+
17
+ interface AssetMetadata {
18
+ version: string;
19
+ assets: Array<{ path: string; 'content-hash': string }>;
20
+ }
21
+
22
+ /**
23
+ * Reads asset metadata from .flex-cli/asset-meta.edn
24
+ */
25
+ function readAssetMetadata(basePath: string): AssetMetadata | null {
26
+ const metaPath = join(basePath, '.flex-cli', 'asset-meta.edn');
27
+ if (!existsSync(metaPath)) {
28
+ return null;
29
+ }
30
+
31
+ try {
32
+ const content = readFileSync(metaPath, 'utf-8');
33
+ const parsed = edn.parse(content);
34
+
35
+ // Convert EDN map to JavaScript object
36
+ const version = parsed.at(edn.kw(':version'));
37
+ const assets = parsed.at(edn.kw(':assets'));
38
+
39
+ const assetList: Array<{ path: string; 'content-hash': string }> = [];
40
+ if (assets && assets.val) {
41
+ for (const asset of assets.val) {
42
+ assetList.push({
43
+ path: asset.at(edn.kw(':path')),
44
+ 'content-hash': asset.at(edn.kw(':content-hash')),
45
+ });
46
+ }
47
+ }
48
+
49
+ return { version, assets: assetList };
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Writes asset metadata to .flex-cli/asset-meta.edn
57
+ */
58
+ function writeAssetMetadata(basePath: string, metadata: AssetMetadata): void {
59
+ const metaDir = join(basePath, '.flex-cli');
60
+ if (!existsSync(metaDir)) {
61
+ mkdirSync(metaDir, { recursive: true });
62
+ }
63
+
64
+ const assets = metadata.assets.map(a =>
65
+ new edn.Map([
66
+ edn.kw(':path'), a.path,
67
+ edn.kw(':content-hash'), a['content-hash']
68
+ ])
69
+ );
70
+
71
+ const ednMap = new edn.Map([
72
+ edn.kw(':version'), metadata.version,
73
+ edn.kw(':assets'), new edn.Vector(assets)
74
+ ]);
75
+
76
+ const metaPath = join(basePath, '.flex-cli', 'asset-meta.edn');
77
+ writeFileSync(metaPath, edn.encode(ednMap), 'utf-8');
78
+ }
79
+
80
+ /**
81
+ * Calculates SHA-1 hash of file content
82
+ */
83
+ function calculateHash(data: Buffer): string {
84
+ return createHash('sha1').update(data).digest('hex');
85
+ }
86
+
87
+ /**
88
+ * Reads all assets from a directory
89
+ */
90
+ function readLocalAssets(basePath: string): Array<{ path: string; data: Buffer; hash: string }> {
91
+ const assets: Array<{ path: string; data: Buffer; hash: string }> = [];
92
+
93
+ function scanDir(dir: string, relativePath: string = '') {
94
+ const entries = readdirSync(dir);
95
+
96
+ for (const entry of entries) {
97
+ if (entry === '.flex-cli') continue; // Skip metadata directory
98
+
99
+ const fullPath = join(dir, entry);
100
+ const relPath = relativePath ? join(relativePath, entry) : entry;
101
+ const stat = statSync(fullPath);
102
+
103
+ if (stat.isDirectory()) {
104
+ scanDir(fullPath, relPath);
105
+ } else if (stat.isFile()) {
106
+ const data = readFileSync(fullPath);
107
+ const hash = calculateHash(data);
108
+ assets.push({ path: relPath, data, hash });
109
+ }
110
+ }
111
+ }
112
+
113
+ scanDir(basePath);
114
+ return assets;
115
+ }
116
+
117
+ /**
118
+ * Validates JSON files
119
+ */
120
+ function validateJsonAssets(assets: Array<{ path: string; data: Buffer }>): void {
121
+ for (const asset of assets) {
122
+ if (asset.path.endsWith('.json')) {
123
+ try {
124
+ JSON.parse(asset.data.toString('utf-8'));
125
+ } catch (error) {
126
+ throw new Error(`Invalid JSON in ${asset.path}: ${error}`);
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Pulls assets from remote
134
+ */
135
+ async function pullAssets(
136
+ marketplace: string,
137
+ path: string,
138
+ version?: string,
139
+ prune?: boolean
140
+ ): Promise<void> {
141
+ try {
142
+ // Validate path
143
+ if (!existsSync(path)) {
144
+ mkdirSync(path, { recursive: true });
145
+ }
146
+
147
+ const stat = statSync(path);
148
+ if (!stat.isDirectory()) {
149
+ throw new Error(`${path} is not a directory`);
150
+ }
151
+
152
+ // Fetch assets from API
153
+ const result = await sdkPullAssets(undefined, marketplace, version ? { version } : undefined);
154
+ const remoteVersion = result.version;
155
+
156
+ // Read current metadata
157
+ const currentMeta = readAssetMetadata(path);
158
+
159
+ // Check if up to date
160
+ if (currentMeta && currentMeta.version === remoteVersion && result.assets.length === currentMeta.assets.length) {
161
+ console.log('Assets are up to date.');
162
+ return;
163
+ }
164
+
165
+ // Write assets to disk
166
+ const newAssets: Array<{ path: string; 'content-hash': string }> = [];
167
+ for (const asset of result.assets) {
168
+ const assetPath = join(path, asset.path);
169
+ const assetDir = dirname(assetPath);
170
+
171
+ if (!existsSync(assetDir)) {
172
+ mkdirSync(assetDir, { recursive: true });
173
+ }
174
+
175
+ // Decode base64 data
176
+ const data = Buffer.from(asset.dataRaw, 'base64');
177
+ writeFileSync(assetPath, data);
178
+
179
+ const hash = calculateHash(data);
180
+ newAssets.push({ path: asset.path, 'content-hash': asset.contentHash || hash });
181
+ }
182
+
183
+ // Prune deleted assets if requested
184
+ if (prune && currentMeta) {
185
+ const remotePaths = new Set(result.assets.map(a => a.path));
186
+ for (const localAsset of currentMeta.assets) {
187
+ if (!remotePaths.has(localAsset.path)) {
188
+ const assetPath = join(path, localAsset.path);
189
+ if (existsSync(assetPath)) {
190
+ unlinkSync(assetPath);
191
+ }
192
+ }
193
+ }
194
+ }
195
+
196
+ // Update metadata
197
+ writeAssetMetadata(path, {
198
+ version: remoteVersion,
199
+ assets: newAssets,
200
+ });
201
+
202
+ console.log(`Version ${remoteVersion} successfully pulled.`);
203
+ } catch (error) {
204
+ if (error && typeof error === 'object' && 'message' in error) {
205
+ printError(error.message as string);
206
+ } else {
207
+ printError('Failed to pull assets');
208
+ }
209
+ process.exit(1);
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Pushes assets to remote
215
+ */
216
+ async function pushAssets(
217
+ marketplace: string,
218
+ path: string,
219
+ prune?: boolean
220
+ ): Promise<void> {
221
+ try {
222
+ // Validate path
223
+ if (!existsSync(path) || !statSync(path).isDirectory()) {
224
+ throw new Error(`${path} is not a valid directory`);
225
+ }
226
+
227
+ // Read current metadata
228
+ const currentMeta = readAssetMetadata(path);
229
+ const currentVersion = currentMeta?.version || 'nil';
230
+
231
+ // Read local assets
232
+ const localAssets = readLocalAssets(path);
233
+
234
+ // Validate JSON files
235
+ validateJsonAssets(localAssets);
236
+
237
+ // Build operations
238
+ const operations: Array<{
239
+ path: string;
240
+ op: 'upsert' | 'delete';
241
+ data?: Buffer;
242
+ }> = [];
243
+
244
+ // Find assets to upsert (new or changed)
245
+ const localAssetMap = new Map(localAssets.map(a => [a.path, a]));
246
+ const currentAssetMap = new Map((currentMeta?.assets || []).map(a => [a.path, a['content-hash']]));
247
+
248
+ for (const [assetPath, asset] of localAssetMap) {
249
+ const currentHash = currentAssetMap.get(assetPath);
250
+ if (!currentHash || currentHash !== asset.hash) {
251
+ operations.push({
252
+ path: assetPath,
253
+ op: 'upsert',
254
+ data: asset.data,
255
+ });
256
+ }
257
+ }
258
+
259
+ // Find assets to delete (if prune enabled)
260
+ if (prune && currentMeta) {
261
+ for (const currentAsset of currentMeta.assets) {
262
+ if (!localAssetMap.has(currentAsset.path)) {
263
+ operations.push({
264
+ path: currentAsset.path,
265
+ op: 'delete',
266
+ });
267
+ }
268
+ }
269
+ }
270
+
271
+ // Check if there are any changes
272
+ if (operations.length === 0) {
273
+ console.log('Assets are up to date.');
274
+ return;
275
+ }
276
+
277
+ // Upload to API
278
+ const result = await sdkPushAssets(undefined, marketplace, currentVersion, operations);
279
+
280
+ // Update local metadata
281
+ writeAssetMetadata(path, {
282
+ version: result.version,
283
+ assets: result.assets.map(a => ({
284
+ path: a.path,
285
+ 'content-hash': a.contentHash,
286
+ })),
287
+ });
288
+
289
+ console.log(`New version ${result.version} successfully created.`);
290
+ } catch (error) {
291
+ if (error && typeof error === 'object' && 'message' in error) {
292
+ printError(error.message as string);
293
+ } else {
294
+ printError('Failed to push assets');
295
+ }
296
+ process.exit(1);
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Registers assets commands
302
+ */
303
+ export function registerAssetsCommands(program: Command): void {
304
+ const assetsCmd = program.command('assets').description('manage marketplace assets');
305
+
306
+ // assets pull
307
+ assetsCmd
308
+ .command('pull')
309
+ .description('pull assets from remote')
310
+ .requiredOption('--path <PATH>', 'path to directory where assets will be stored')
311
+ .option('--version <VERSION>', 'version of assets to pull')
312
+ .option('--prune', 'delete local files no longer present as remote assets')
313
+ .option('-m, --marketplace <MARKETPLACE_ID>', 'marketplace identifier')
314
+ .action(async (opts) => {
315
+ const marketplace = opts.marketplace || program.opts().marketplace;
316
+ if (!marketplace) {
317
+ console.error('Error: --marketplace is required');
318
+ process.exit(1);
319
+ }
320
+ await pullAssets(marketplace, opts.path, opts.version, opts.prune);
321
+ });
322
+
323
+ // assets push
324
+ assetsCmd
325
+ .command('push')
326
+ .description('push assets to remote')
327
+ .requiredOption('--path <PATH>', 'path to directory with assets')
328
+ .option('--prune', 'delete remote assets no longer present locally')
329
+ .option('-m, --marketplace <MARKETPLACE_ID>', 'marketplace identifier')
330
+ .action(async (opts) => {
331
+ const marketplace = opts.marketplace || program.opts().marketplace;
332
+ if (!marketplace) {
333
+ console.error('Error: --marketplace is required');
334
+ process.exit(1);
335
+ }
336
+ await pushAssets(marketplace, opts.path, opts.prune);
337
+ });
338
+ }
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Events command - query marketplace events
3
+ */
4
+
5
+ import { Command } from 'commander';
6
+ import { printTable, printError } from '../../util/output.js';
7
+ import {
8
+ queryEvents as sdkQueryEvents,
9
+ pollEvents as sdkPollEvents,
10
+ type EventData as SdkEventData
11
+ } from 'sharetribe-flex-build-sdk';
12
+
13
+ interface EventsQueryOptions {
14
+ resourceId?: string;
15
+ relatedResourceId?: string;
16
+ eventTypes?: string;
17
+ sequenceId?: number;
18
+ afterSeqId?: number;
19
+ beforeSeqId?: number;
20
+ afterTs?: string;
21
+ beforeTs?: string;
22
+ limit?: number;
23
+ json?: boolean;
24
+ jsonPretty?: boolean;
25
+ }
26
+
27
+ /**
28
+ * Validates query parameters
29
+ */
30
+ function validateParams(opts: EventsQueryOptions): void {
31
+ const exclusiveParams = [
32
+ opts.sequenceId !== undefined,
33
+ opts.afterSeqId !== undefined,
34
+ opts.beforeSeqId !== undefined,
35
+ opts.afterTs !== undefined,
36
+ opts.beforeTs !== undefined,
37
+ ];
38
+
39
+ if (exclusiveParams.filter(Boolean).length > 1) {
40
+ throw new Error(
41
+ 'Only one of --seqid, --after-seqid, --before-seqid, --after-ts, or --before-ts can be specified'
42
+ );
43
+ }
44
+
45
+ if (opts.resourceId && opts.relatedResourceId) {
46
+ throw new Error('Only one of --resource or --related-resource can be specified');
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Formats timestamp to match flex-cli format: YYYY-MM-DD H:MM:SS AM/PM
52
+ */
53
+ function formatTimestamp(timestamp: string): string {
54
+ try {
55
+ const date = new Date(timestamp);
56
+ const year = date.getFullYear();
57
+ const month = String(date.getMonth() + 1).padStart(2, '0');
58
+ const day = String(date.getDate()).padStart(2, '0');
59
+ const timeString = date.toLocaleTimeString('en-US');
60
+
61
+ return `${year}-${month}-${day} ${timeString}`;
62
+ } catch {
63
+ return timestamp;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Queries events from API
69
+ */
70
+ async function queryEvents(
71
+ marketplace: string,
72
+ opts: EventsQueryOptions
73
+ ): Promise<void> {
74
+ try {
75
+ validateParams(opts);
76
+
77
+ const events = await sdkQueryEvents(
78
+ undefined, // Use auth from file
79
+ marketplace,
80
+ {
81
+ resourceId: opts.resourceId,
82
+ relatedResourceId: opts.relatedResourceId,
83
+ eventTypes: opts.eventTypes,
84
+ sequenceId: opts.sequenceId,
85
+ afterSeqId: opts.afterSeqId,
86
+ beforeSeqId: opts.beforeSeqId,
87
+ afterTs: opts.afterTs,
88
+ beforeTs: opts.beforeTs,
89
+ limit: opts.limit,
90
+ }
91
+ );
92
+
93
+ if (events.length === 0) {
94
+ console.log('No events found.');
95
+ return;
96
+ }
97
+
98
+ // Output format
99
+ if (opts.json) {
100
+ for (const event of events) {
101
+ // Exclude auditEmails to match flex-cli JSON format
102
+ const { auditEmails, ...eventWithoutEmails } = event;
103
+ console.log(JSON.stringify(eventWithoutEmails));
104
+ }
105
+ } else if (opts.jsonPretty) {
106
+ for (const event of events) {
107
+ // Exclude auditEmails to match flex-cli JSON format
108
+ const { auditEmails, ...eventWithoutEmails } = event;
109
+ console.log(JSON.stringify(eventWithoutEmails, null, 2));
110
+ }
111
+ } else {
112
+ printTable(
113
+ ['Seq ID', 'Resource ID', 'Event type', 'Created at local time', 'Source', 'Actor'],
114
+ events.map((event) => {
115
+ const actor = event.auditEmails?.userEmail || event.auditEmails?.adminEmail || '';
116
+ const source = event.source?.replace('source/', '') || '';
117
+
118
+ return {
119
+ 'Seq ID': event.sequenceId.toString(),
120
+ 'Resource ID': event.resourceId,
121
+ 'Event type': event.eventType,
122
+ 'Created at local time': formatTimestamp(event.createdAt),
123
+ 'Source': source,
124
+ 'Actor': actor,
125
+ };
126
+ })
127
+ );
128
+ }
129
+ } catch (error) {
130
+ if (error && typeof error === 'object' && 'message' in error) {
131
+ printError(error.message as string);
132
+ } else {
133
+ printError('Failed to query events');
134
+ }
135
+ process.exit(1);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Tails events (live streaming)
141
+ */
142
+ async function tailEvents(
143
+ marketplace: string,
144
+ opts: EventsQueryOptions
145
+ ): Promise<void> {
146
+ try {
147
+ validateParams(opts);
148
+
149
+ console.log('Tailing events... Press Ctrl+C to stop');
150
+ console.log('');
151
+
152
+ const stopPolling = sdkPollEvents(
153
+ undefined, // Use auth from file
154
+ marketplace,
155
+ {
156
+ resourceId: opts.resourceId,
157
+ relatedResourceId: opts.relatedResourceId,
158
+ eventTypes: opts.eventTypes,
159
+ limit: opts.limit || 10,
160
+ },
161
+ (events: SdkEventData[]) => {
162
+ // Output events
163
+ if (opts.json) {
164
+ for (const event of events) {
165
+ // Exclude auditEmails to match flex-cli JSON format
166
+ const { auditEmails, ...eventWithoutEmails } = event;
167
+ console.log(JSON.stringify(eventWithoutEmails));
168
+ }
169
+ } else if (opts.jsonPretty) {
170
+ for (const event of events) {
171
+ // Exclude auditEmails to match flex-cli JSON format
172
+ const { auditEmails, ...eventWithoutEmails } = event;
173
+ console.log(JSON.stringify(eventWithoutEmails, null, 2));
174
+ }
175
+ } else {
176
+ printTable(
177
+ ['Seq ID', 'Resource ID', 'Event type', 'Created at local time', 'Source', 'Actor'],
178
+ events.map((event) => {
179
+ const actor = event.auditEmails?.userEmail || event.auditEmails?.adminEmail || '';
180
+ const source = event.source?.replace('source/', '') || '';
181
+
182
+ return {
183
+ 'Seq ID': event.sequenceId.toString(),
184
+ 'Resource ID': event.resourceId,
185
+ 'Event type': event.eventType,
186
+ 'Created at local time': formatTimestamp(event.createdAt),
187
+ 'Source': source,
188
+ 'Actor': actor,
189
+ };
190
+ })
191
+ );
192
+ }
193
+ },
194
+ 5000 // 5 second poll interval
195
+ );
196
+
197
+ // Handle graceful shutdown
198
+ const shutdown = () => {
199
+ console.log('\nStopping tail...');
200
+ stopPolling();
201
+ process.exit(0);
202
+ };
203
+
204
+ process.on('SIGINT', shutdown);
205
+ process.on('SIGTERM', shutdown);
206
+
207
+ } catch (error) {
208
+ if (error && typeof error === 'object' && 'message' in error) {
209
+ printError(error.message as string);
210
+ } else {
211
+ printError('Failed to tail events');
212
+ }
213
+ process.exit(1);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Registers events command
219
+ */
220
+ export function registerEventsCommand(program: Command): void {
221
+ const cmd = program
222
+ .command('events')
223
+ .description('Get a list of events.')
224
+ .option('--resource <RESOURCE_ID>', 'show events for specific resource ID')
225
+ .option('--related-resource <RELATED_RESOURCE_ID>', 'show events related to specific resource ID')
226
+ .option('--filter <EVENT_TYPES>', 'filter by event types (comma-separated)')
227
+ .option('--seqid <SEQUENCE_ID>', 'get event with specific sequence ID', parseInt)
228
+ .option('--after-seqid <SEQUENCE_ID>', 'show events after sequence ID (exclusive)', parseInt)
229
+ .option('--before-seqid <SEQUENCE_ID>', 'show events before sequence ID (exclusive)', parseInt)
230
+ .option('--after-ts <TIMESTAMP>', 'show events after timestamp')
231
+ .option('--before-ts <TIMESTAMP>', 'show events before timestamp')
232
+ .option('-l, --limit <NUMBER>', 'limit results (default: 100, max: 100)', parseInt)
233
+ .option('--json', 'output as single-line JSON strings')
234
+ .option('--json-pretty', 'output as indented multi-line JSON')
235
+ .option('-m, --marketplace <MARKETPLACE_ID>', 'marketplace identifier');
236
+
237
+ // Default action - query
238
+ cmd.action(async (opts) => {
239
+ const marketplace = opts.marketplace || program.opts().marketplace;
240
+ if (!marketplace) {
241
+ console.error('Could not parse arguments:');
242
+ console.error('--marketplace is required');
243
+ process.exit(1);
244
+ }
245
+
246
+ await queryEvents(marketplace, {
247
+ resourceId: opts.resource,
248
+ relatedResourceId: opts.relatedResource,
249
+ eventTypes: opts.filter,
250
+ sequenceId: opts.seqid,
251
+ afterSeqId: opts.afterSeqid,
252
+ beforeSeqId: opts.beforeSeqid,
253
+ afterTs: opts.afterTs,
254
+ beforeTs: opts.beforeTs,
255
+ limit: opts.limit || 100,
256
+ json: opts.json,
257
+ jsonPretty: opts.jsonPretty,
258
+ });
259
+ });
260
+
261
+ // tail subcommand
262
+ cmd
263
+ .command('tail')
264
+ .description('Tail events live as they happen')
265
+ .option('--resource <RESOURCE_ID>', 'show events for specific resource ID')
266
+ .option('--related-resource <RELATED_RESOURCE_ID>', 'show events related to specific resource ID')
267
+ .option('--filter <EVENT_TYPES>', 'filter by event types (comma-separated)')
268
+ .option('-l, --limit <NUMBER>', 'limit results per poll (default: 10, max: 100)', parseInt)
269
+ .option('--json', 'output as single-line JSON strings')
270
+ .option('--json-pretty', 'output as indented multi-line JSON')
271
+ .option('-m, --marketplace <MARKETPLACE_ID>', 'marketplace identifier')
272
+ .action(async (opts) => {
273
+ const marketplace = opts.marketplace || program.opts().marketplace;
274
+ if (!marketplace) {
275
+ console.error('Could not parse arguments:');
276
+ console.error('--marketplace is required');
277
+ process.exit(1);
278
+ }
279
+
280
+ await tailEvents(marketplace, {
281
+ resourceId: opts.resource,
282
+ relatedResourceId: opts.relatedResource,
283
+ eventTypes: opts.filter,
284
+ limit: opts.limit || 10,
285
+ json: opts.json,
286
+ jsonPretty: opts.jsonPretty,
287
+ });
288
+ });
289
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Help command - displays help information
3
+ *
4
+ * Note: Top-level help may differ from flex-cli due to new commands
5
+ * Subcommand help must match flex-cli exactly
6
+ */
7
+
8
+ import { Command } from 'commander';
9
+
10
+ /**
11
+ * Registers help command
12
+ *
13
+ * Commander.js handles this automatically, but we can customize if needed
14
+ */
15
+ export function registerHelpCommand(program: Command): void {
16
+ // Commander.js provides built-in help functionality
17
+ // This function exists for future customization if needed
18
+ program.addHelpCommand('help [command]', 'display help for command');
19
+ }