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/.eslintrc.json +29 -0
- package/.prettierrc +9 -0
- package/build.js +58 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +7 -0
- package/package.json +58 -0
- package/src/commands/assets/index.ts +338 -0
- package/src/commands/events/index.ts +289 -0
- package/src/commands/help.ts +19 -0
- package/src/commands/listing-approval.ts +121 -0
- package/src/commands/login.ts +43 -0
- package/src/commands/logout.ts +17 -0
- package/src/commands/notifications/index.ts +221 -0
- package/src/commands/process/aliases.ts +82 -0
- package/src/commands/process/combined.ts +62 -0
- package/src/commands/process/create.ts +35 -0
- package/src/commands/process/index.ts +309 -0
- package/src/commands/process/list.ts +75 -0
- package/src/commands/process/pull.ts +81 -0
- package/src/commands/process/push.ts +67 -0
- package/src/commands/search/index.ts +254 -0
- package/src/commands/stripe/index.ts +114 -0
- package/src/commands/version.ts +40 -0
- package/src/index.ts +131 -0
- package/src/types/index.ts +21 -0
- package/src/util/command-router.ts +41 -0
- package/src/util/help-formatter.ts +266 -0
- package/src/util/output.ts +83 -0
- package/test/help-comparison.test.ts +255 -0
- package/test/process-builder.test.ts +14 -0
- package/test/process-integration.test.ts +189 -0
- package/test/strict-comparison.test.ts +722 -0
- package/tsconfig.json +50 -0
- package/vitest.config.ts +12 -0
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
|
+
}
|