sharetribe-cli 1.15.2 → 1.16.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/dist/index.js +12 -10
- package/dist/index.js.map +3 -3
- package/package.json +4 -2
- package/src/commands/assets/index.ts +291 -55
- package/src/commands/debug.ts +6 -16
- package/src/commands/search/index.ts +19 -7
- package/test/assets.test.ts +15 -0
- package/test/help-comparison.test.ts +29 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sharetribe-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Unofficial Sharetribe CLI - 100% compatible with flex-cli",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -42,11 +42,13 @@
|
|
|
42
42
|
"inquirer": "^9.2.23",
|
|
43
43
|
"jsedn": "^0.4.1",
|
|
44
44
|
"sharetribe-flex-build-sdk": "^1.15.2",
|
|
45
|
-
"yargs": "^18.0.0"
|
|
45
|
+
"yargs": "^18.0.0",
|
|
46
|
+
"yauzl": "^3.2.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@types/inquirer": "^9.0.7",
|
|
49
50
|
"@types/node": "^20.17.10",
|
|
51
|
+
"@types/yauzl": "^2.10.3",
|
|
50
52
|
"@typescript-eslint/eslint-plugin": "^8.18.2",
|
|
51
53
|
"@typescript-eslint/parser": "^8.18.2",
|
|
52
54
|
"esbuild": "^0.27.2",
|
|
@@ -4,16 +4,31 @@
|
|
|
4
4
|
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import {
|
|
7
|
-
pullAssets as sdkPullAssets,
|
|
8
7
|
pushAssets as sdkPushAssets,
|
|
9
8
|
stageAsset as sdkStageAsset,
|
|
9
|
+
getApiBaseUrl,
|
|
10
|
+
readAuth,
|
|
10
11
|
} from 'sharetribe-flex-build-sdk';
|
|
11
12
|
import { printError } from '../../util/output.js';
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
readFileSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
existsSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
readdirSync,
|
|
19
|
+
statSync,
|
|
20
|
+
unlinkSync,
|
|
21
|
+
createWriteStream,
|
|
22
|
+
} from 'node:fs';
|
|
13
23
|
import { join, dirname } from 'node:path';
|
|
14
24
|
import { createHash } from 'node:crypto';
|
|
25
|
+
import * as http from 'node:http';
|
|
26
|
+
import * as https from 'node:https';
|
|
27
|
+
import { tmpdir } from 'node:os';
|
|
28
|
+
import { pipeline } from 'node:stream/promises';
|
|
15
29
|
import chalk from 'chalk';
|
|
16
30
|
import edn from 'jsedn';
|
|
31
|
+
import yauzl from 'yauzl';
|
|
17
32
|
|
|
18
33
|
|
|
19
34
|
interface AssetMetadata {
|
|
@@ -21,21 +36,15 @@ interface AssetMetadata {
|
|
|
21
36
|
assets: Array<{ path: string; 'content-hash': string }>;
|
|
22
37
|
}
|
|
23
38
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const metaPath = join(basePath, '.flex-cli', 'asset-meta.edn');
|
|
29
|
-
if (!existsSync(metaPath)) {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
39
|
+
const ASSET_META_FILENAME = 'meta/asset-meta.edn';
|
|
40
|
+
const ASSETS_DIR = 'assets/';
|
|
41
|
+
const CLEAR_LINE = '\x1b[K';
|
|
42
|
+
const CARRIAGE_RETURN = '\r';
|
|
32
43
|
|
|
44
|
+
function parseAssetMetadataEdn(content: string): AssetMetadata | null {
|
|
33
45
|
try {
|
|
34
|
-
const content = readFileSync(metaPath, 'utf-8');
|
|
35
46
|
const parsed = edn.parse(content);
|
|
36
|
-
|
|
37
|
-
// Convert EDN map to JavaScript object
|
|
38
|
-
const version = parsed.at(edn.kw(':version'));
|
|
47
|
+
const version = parsed.at(edn.kw(':version')) || parsed.at(edn.kw(':aliased-version'));
|
|
39
48
|
const assets = parsed.at(edn.kw(':assets'));
|
|
40
49
|
|
|
41
50
|
const assetList: Array<{ path: string; 'content-hash': string }> = [];
|
|
@@ -48,12 +57,33 @@ function readAssetMetadata(basePath: string): AssetMetadata | null {
|
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
|
|
60
|
+
if (!version) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
return { version, assets: assetList };
|
|
52
65
|
} catch {
|
|
53
66
|
return null;
|
|
54
67
|
}
|
|
55
68
|
}
|
|
56
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Reads asset metadata from .flex-cli/asset-meta.edn
|
|
72
|
+
*/
|
|
73
|
+
function readAssetMetadata(basePath: string): AssetMetadata | null {
|
|
74
|
+
const metaPath = join(basePath, '.flex-cli', 'asset-meta.edn');
|
|
75
|
+
if (!existsSync(metaPath)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const content = readFileSync(metaPath, 'utf-8');
|
|
81
|
+
return parseAssetMetadataEdn(content);
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
57
87
|
/**
|
|
58
88
|
* Writes asset metadata to .flex-cli/asset-meta.edn
|
|
59
89
|
*/
|
|
@@ -119,6 +149,35 @@ function readLocalAssets(basePath: string): Array<{ path: string; data: Buffer;
|
|
|
119
149
|
return assets;
|
|
120
150
|
}
|
|
121
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Lists local asset paths without reading file data
|
|
154
|
+
*/
|
|
155
|
+
function listLocalAssetPaths(basePath: string): string[] {
|
|
156
|
+
const assets: string[] = [];
|
|
157
|
+
|
|
158
|
+
function scanDir(dir: string, relativePath: string = '') {
|
|
159
|
+
const entries = readdirSync(dir);
|
|
160
|
+
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
if (entry === '.flex-cli') continue;
|
|
163
|
+
if (entry === '.DS_Store') continue;
|
|
164
|
+
|
|
165
|
+
const fullPath = join(dir, entry);
|
|
166
|
+
const relPath = relativePath ? join(relativePath, entry) : entry;
|
|
167
|
+
const stat = statSync(fullPath);
|
|
168
|
+
|
|
169
|
+
if (stat.isDirectory()) {
|
|
170
|
+
scanDir(fullPath, relPath);
|
|
171
|
+
} else if (stat.isFile()) {
|
|
172
|
+
assets.push(relPath);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
scanDir(basePath);
|
|
178
|
+
return assets;
|
|
179
|
+
}
|
|
180
|
+
|
|
122
181
|
/**
|
|
123
182
|
* Validates JSON files
|
|
124
183
|
*/
|
|
@@ -134,6 +193,186 @@ function validateJsonAssets(assets: Array<{ path: string; data: Buffer }>): void
|
|
|
134
193
|
}
|
|
135
194
|
}
|
|
136
195
|
|
|
196
|
+
function formatDownloadProgress(bytes: number): string {
|
|
197
|
+
const mb = bytes / 1024 / 1024;
|
|
198
|
+
return `${CARRIAGE_RETURN}${CLEAR_LINE}Downloaded ${mb.toFixed(2)}MB`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function printDownloadProgress(stream: NodeJS.ReadableStream): void {
|
|
202
|
+
let downloaded = 0;
|
|
203
|
+
const printProgress = (): void => {
|
|
204
|
+
process.stderr.write(formatDownloadProgress(downloaded));
|
|
205
|
+
};
|
|
206
|
+
const interval = setInterval(printProgress, 100);
|
|
207
|
+
|
|
208
|
+
stream.on('data', (chunk: Buffer) => {
|
|
209
|
+
downloaded += chunk.length;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
stream.on('end', () => {
|
|
213
|
+
clearInterval(interval);
|
|
214
|
+
printProgress();
|
|
215
|
+
process.stderr.write('\nFinished downloading assets\n');
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function getApiKeyOrThrow(): string {
|
|
220
|
+
const auth = readAuth();
|
|
221
|
+
if (!auth?.apiKey) {
|
|
222
|
+
throw new Error('Not logged in. Please provide apiKey or run: sharetribe-cli login');
|
|
223
|
+
}
|
|
224
|
+
return auth.apiKey;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function getAssetsPullUrl(marketplace: string, version?: string): URL {
|
|
228
|
+
const url = new URL(getApiBaseUrl() + '/assets/pull');
|
|
229
|
+
url.searchParams.set('marketplace', marketplace);
|
|
230
|
+
if (version) {
|
|
231
|
+
url.searchParams.set('version', version);
|
|
232
|
+
} else {
|
|
233
|
+
url.searchParams.set('version-alias', 'latest');
|
|
234
|
+
}
|
|
235
|
+
return url;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function getErrorMessage(body: string, statusCode: number): string {
|
|
239
|
+
try {
|
|
240
|
+
const parsed = JSON.parse(body) as { errors?: Array<{ message?: string }> };
|
|
241
|
+
const message = parsed.errors?.[0]?.message;
|
|
242
|
+
if (message) {
|
|
243
|
+
return message;
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
// Ignore JSON parse errors
|
|
247
|
+
}
|
|
248
|
+
return body || `HTTP ${statusCode}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function getAssetsZipStream(
|
|
252
|
+
marketplace: string,
|
|
253
|
+
version?: string
|
|
254
|
+
): Promise<http.IncomingMessage> {
|
|
255
|
+
const url = getAssetsPullUrl(marketplace, version);
|
|
256
|
+
const apiKey = getApiKeyOrThrow();
|
|
257
|
+
const isHttps = url.protocol === 'https:';
|
|
258
|
+
const client = isHttps ? https : http;
|
|
259
|
+
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
const req = client.request(
|
|
262
|
+
{
|
|
263
|
+
method: 'GET',
|
|
264
|
+
hostname: url.hostname,
|
|
265
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
266
|
+
path: url.pathname + url.search,
|
|
267
|
+
headers: {
|
|
268
|
+
Authorization: `Apikey ${apiKey}`,
|
|
269
|
+
Accept: 'application/zip',
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
(res) => {
|
|
273
|
+
const statusCode = res.statusCode || 0;
|
|
274
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
275
|
+
const chunks: Buffer[] = [];
|
|
276
|
+
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
277
|
+
res.on('end', () => {
|
|
278
|
+
const body = Buffer.concat(chunks).toString('utf-8');
|
|
279
|
+
reject(new Error(getErrorMessage(body, statusCode)));
|
|
280
|
+
});
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
resolve(res);
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
req.setTimeout(120000, () => {
|
|
288
|
+
req.destroy(new Error('Request timeout'));
|
|
289
|
+
});
|
|
290
|
+
req.on('error', reject);
|
|
291
|
+
req.end();
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function createTempZipPath(): string {
|
|
296
|
+
return join(tmpdir(), `assets-${Date.now()}.zip`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function removeAssetsDir(filename: string): string {
|
|
300
|
+
if (filename.startsWith(ASSETS_DIR)) {
|
|
301
|
+
return filename.slice(ASSETS_DIR.length);
|
|
302
|
+
}
|
|
303
|
+
return filename;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function readStreamToString(stream: NodeJS.ReadableStream): Promise<string> {
|
|
307
|
+
return new Promise((resolve, reject) => {
|
|
308
|
+
const chunks: Buffer[] = [];
|
|
309
|
+
stream.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
310
|
+
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
311
|
+
stream.on('error', reject);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function unzipAssets(zipPath: string, basePath: string): Promise<AssetMetadata> {
|
|
316
|
+
return new Promise((resolve, reject) => {
|
|
317
|
+
yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
|
|
318
|
+
if (err || !zipfile) {
|
|
319
|
+
reject(err || new Error('Failed to open zip file'));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let assetMeta: AssetMetadata | null = null;
|
|
324
|
+
|
|
325
|
+
zipfile.on('error', reject);
|
|
326
|
+
zipfile.on('end', () => {
|
|
327
|
+
if (!assetMeta) {
|
|
328
|
+
reject(new Error('Asset metadata not found in zip'));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
resolve(assetMeta);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
zipfile.readEntry();
|
|
335
|
+
zipfile.on('entry', (entry) => {
|
|
336
|
+
if (entry.fileName.endsWith('/')) {
|
|
337
|
+
zipfile.readEntry();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
zipfile.openReadStream(entry, (streamErr, readStream) => {
|
|
342
|
+
if (streamErr || !readStream) {
|
|
343
|
+
reject(streamErr || new Error('Failed to read zip entry'));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (entry.fileName === ASSET_META_FILENAME) {
|
|
348
|
+
readStreamToString(readStream)
|
|
349
|
+
.then((content) => {
|
|
350
|
+
assetMeta = parseAssetMetadataEdn(content);
|
|
351
|
+
if (!assetMeta) {
|
|
352
|
+
reject(new Error('Invalid asset metadata'));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
zipfile.readEntry();
|
|
356
|
+
})
|
|
357
|
+
.catch(reject);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const assetPath = join(basePath, removeAssetsDir(entry.fileName));
|
|
362
|
+
const assetDir = dirname(assetPath);
|
|
363
|
+
if (!existsSync(assetDir)) {
|
|
364
|
+
mkdirSync(assetDir, { recursive: true });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
pipeline(readStream, createWriteStream(assetPath))
|
|
368
|
+
.then(() => zipfile.readEntry())
|
|
369
|
+
.catch(reject);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
137
376
|
/**
|
|
138
377
|
* Pulls assets from remote
|
|
139
378
|
*/
|
|
@@ -154,57 +393,48 @@ async function pullAssets(
|
|
|
154
393
|
throw new Error(`${path} is not a directory`);
|
|
155
394
|
}
|
|
156
395
|
|
|
157
|
-
|
|
158
|
-
const result = await sdkPullAssets(undefined, marketplace, version ? { version } : undefined);
|
|
159
|
-
const remoteVersion = result.version;
|
|
160
|
-
|
|
161
|
-
// Read current metadata
|
|
396
|
+
const localAssets = prune ? listLocalAssetPaths(path) : [];
|
|
162
397
|
const currentMeta = readAssetMetadata(path);
|
|
398
|
+
const tempZipPath = createTempZipPath();
|
|
163
399
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
400
|
+
try {
|
|
401
|
+
const zipStream = await getAssetsZipStream(marketplace, version);
|
|
402
|
+
printDownloadProgress(zipStream);
|
|
403
|
+
await pipeline(zipStream, createWriteStream(tempZipPath));
|
|
169
404
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
for (const asset of result.assets) {
|
|
173
|
-
const assetPath = join(path, asset.path);
|
|
174
|
-
const assetDir = dirname(assetPath);
|
|
175
|
-
|
|
176
|
-
if (!existsSync(assetDir)) {
|
|
177
|
-
mkdirSync(assetDir, { recursive: true });
|
|
178
|
-
}
|
|
405
|
+
const newAssetMeta = await unzipAssets(tempZipPath, path);
|
|
406
|
+
const remoteVersion = newAssetMeta.version;
|
|
179
407
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
408
|
+
const deletedPaths = prune
|
|
409
|
+
? new Set(localAssets.filter(p => !newAssetMeta.assets.some(a => a.path === p)))
|
|
410
|
+
: new Set<string>();
|
|
183
411
|
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
}
|
|
412
|
+
const updated = currentMeta?.version !== remoteVersion;
|
|
413
|
+
const shouldReportUpdate = updated || deletedPaths.size > 0;
|
|
187
414
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const assetPath = join(path, localAsset.path);
|
|
194
|
-
if (existsSync(assetPath)) {
|
|
195
|
-
unlinkSync(assetPath);
|
|
415
|
+
if (deletedPaths.size > 0) {
|
|
416
|
+
for (const assetPath of deletedPaths) {
|
|
417
|
+
const fullPath = join(path, assetPath);
|
|
418
|
+
if (existsSync(fullPath)) {
|
|
419
|
+
unlinkSync(fullPath);
|
|
196
420
|
}
|
|
197
421
|
}
|
|
198
422
|
}
|
|
199
|
-
}
|
|
200
423
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
424
|
+
if (shouldReportUpdate) {
|
|
425
|
+
writeAssetMetadata(path, {
|
|
426
|
+
version: remoteVersion,
|
|
427
|
+
assets: newAssetMeta.assets,
|
|
428
|
+
});
|
|
429
|
+
console.log(`Version ${remoteVersion} successfully pulled.`);
|
|
430
|
+
} else {
|
|
431
|
+
console.log('Assets are up to date.');
|
|
432
|
+
}
|
|
433
|
+
} finally {
|
|
434
|
+
if (existsSync(tempZipPath)) {
|
|
435
|
+
unlinkSync(tempZipPath);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
208
438
|
} catch (error) {
|
|
209
439
|
if (error && typeof error === 'object' && 'message' in error) {
|
|
210
440
|
printError(error.message as string);
|
|
@@ -396,3 +626,9 @@ export function registerAssetsCommands(program: Command): void {
|
|
|
396
626
|
await pushAssets(marketplace, opts.path, opts.prune);
|
|
397
627
|
});
|
|
398
628
|
}
|
|
629
|
+
|
|
630
|
+
export const __test__ = {
|
|
631
|
+
formatDownloadProgress,
|
|
632
|
+
removeAssetsDir,
|
|
633
|
+
parseAssetMetadataEdn,
|
|
634
|
+
};
|
package/src/commands/debug.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Debug command - display config and auth info
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import edn from 'jsedn';
|
|
6
5
|
import { getConfigMap, readAuth } from 'sharetribe-flex-build-sdk';
|
|
7
6
|
|
|
8
7
|
function maskLast4(value: string): string {
|
|
@@ -12,25 +11,16 @@ function maskLast4(value: string): string {
|
|
|
12
11
|
return `...${value.slice(-4)}`;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
function toEdnMap(record: Record<string, string>): edn.Map {
|
|
16
|
-
const entries: Array<unknown> = [];
|
|
17
|
-
for (const [key, value] of Object.entries(record)) {
|
|
18
|
-
entries.push(edn.kw(`:${key}`), value);
|
|
19
|
-
}
|
|
20
|
-
return new edn.Map(entries);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
14
|
export function debug(): void {
|
|
24
15
|
const auth = readAuth();
|
|
25
16
|
const apiKey = auth?.apiKey ? maskLast4(auth.apiKey) : 'No API key set';
|
|
26
17
|
const confMap = getConfigMap();
|
|
27
18
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
]);
|
|
19
|
+
const confMapEntries = Object.keys(confMap)
|
|
20
|
+
.sort()
|
|
21
|
+
.map((key) => `:${key} ${confMap[key]}`)
|
|
22
|
+
.join(' ');
|
|
23
|
+
const confMapFormatted = confMapEntries ? `{${confMapEntries}}` : '{}';
|
|
34
24
|
|
|
35
|
-
console.log(
|
|
25
|
+
console.log(`{:api-key ${apiKey}, :conf-map ${confMapFormatted}}`);
|
|
36
26
|
}
|
|
@@ -208,12 +208,18 @@ export function registerSearchCommands(program: Command): void {
|
|
|
208
208
|
searchCmd
|
|
209
209
|
.command('set')
|
|
210
210
|
.description('set search schema')
|
|
211
|
-
.requiredOption('--key <KEY>', '
|
|
212
|
-
.requiredOption(
|
|
213
|
-
|
|
211
|
+
.requiredOption('--key <KEY>', 'key name')
|
|
212
|
+
.requiredOption(
|
|
213
|
+
'--scope <SCOPE>',
|
|
214
|
+
'extended data scope (either metadata or public for listing schema, metadata, private, protected or public for userProfile schema, metadata or protected for transaction schema)'
|
|
215
|
+
)
|
|
216
|
+
.requiredOption('--type <TYPE>', 'value type (either enum, multi-enum, boolean, long or text)')
|
|
214
217
|
.option('--doc <DOC>', 'description of the schema')
|
|
215
218
|
.option('--default <DEFAULT>', 'default value for search if value is not set')
|
|
216
|
-
.option(
|
|
219
|
+
.option(
|
|
220
|
+
'--schema-for <SCHEMA_FOR>',
|
|
221
|
+
'Subject of the schema (either listing, userProfile or transaction, defaults to listing)'
|
|
222
|
+
)
|
|
217
223
|
.option('-m, --marketplace <MARKETPLACE_ID>', 'marketplace identifier')
|
|
218
224
|
.action(async (opts) => {
|
|
219
225
|
const marketplace = opts.marketplace || program.opts().marketplace;
|
|
@@ -235,9 +241,15 @@ export function registerSearchCommands(program: Command): void {
|
|
|
235
241
|
searchCmd
|
|
236
242
|
.command('unset')
|
|
237
243
|
.description('unset search schema')
|
|
238
|
-
.requiredOption('--key <KEY>', '
|
|
239
|
-
.requiredOption(
|
|
240
|
-
|
|
244
|
+
.requiredOption('--key <KEY>', 'key name')
|
|
245
|
+
.requiredOption(
|
|
246
|
+
'--scope <SCOPE>',
|
|
247
|
+
'extended data scope (either metadata or public for listing schema, metadata, private, protected or public for userProfile schema, metadata or protected for transaction schema)'
|
|
248
|
+
)
|
|
249
|
+
.option(
|
|
250
|
+
'--schema-for <SCHEMA_FOR>',
|
|
251
|
+
'Subject of the schema (either listing, userProfile or transaction, defaults to listing)'
|
|
252
|
+
)
|
|
241
253
|
.option('-m, --marketplace <MARKETPLACE_ID>', 'marketplace identifier')
|
|
242
254
|
.action(async (opts) => {
|
|
243
255
|
const marketplace = opts.marketplace || program.opts().marketplace;
|
package/test/assets.test.ts
CHANGED
|
@@ -7,6 +7,9 @@ import { mkdtempSync, writeFileSync, existsSync, rmSync, readdirSync } from 'fs'
|
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { tmpdir } from 'os';
|
|
9
9
|
import { createHash } from 'node:crypto';
|
|
10
|
+
import { __test__ as assetsTestHelpers } from '../src/commands/assets/index.js';
|
|
11
|
+
|
|
12
|
+
const { formatDownloadProgress } = assetsTestHelpers;
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
15
|
* Calculates SHA-1 hash matching backend convention
|
|
@@ -139,3 +142,15 @@ describe('Asset Type Detection', () => {
|
|
|
139
142
|
expect(isJsonAsset('test.svg')).toBe(false);
|
|
140
143
|
});
|
|
141
144
|
});
|
|
145
|
+
|
|
146
|
+
describe('Asset Pull Progress Output', () => {
|
|
147
|
+
it('formats progress with carriage return and clear line', () => {
|
|
148
|
+
const output = formatDownloadProgress(0);
|
|
149
|
+
expect(output).toBe('\r\x1b[KDownloaded 0.00MB');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('formats progress with two decimal MB values', () => {
|
|
153
|
+
const output = formatDownloadProgress(1024 * 1024);
|
|
154
|
+
expect(output).toBe('\r\x1b[KDownloaded 1.00MB');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -173,6 +173,35 @@ describe('Help Comparison Tests', () => {
|
|
|
173
173
|
});
|
|
174
174
|
});
|
|
175
175
|
|
|
176
|
+
describe('help search set option descriptions', () => {
|
|
177
|
+
it('matches flex-cli wording for key and scope options', () => {
|
|
178
|
+
const shareOutput = runCli('help search set', 'sharetribe');
|
|
179
|
+
expect(shareOutput).toContain('key name');
|
|
180
|
+
expect(shareOutput).toContain('extended data scope (either metadata or public for listing schema,');
|
|
181
|
+
expect(shareOutput).toContain('metadata, private, protected or public for userProfile schema,');
|
|
182
|
+
expect(shareOutput).toContain('metadata or protected for transaction schema)');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('matches flex-cli wording for type and schema-for options', () => {
|
|
186
|
+
const shareOutput = runCli('help search set', 'sharetribe');
|
|
187
|
+
expect(shareOutput).toContain('value type (either enum, multi-enum, boolean, long or text)');
|
|
188
|
+
expect(shareOutput).toContain('Subject of the schema (either listing, userProfile or transaction,');
|
|
189
|
+
expect(shareOutput).toContain('defaults to listing');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('help search unset option descriptions', () => {
|
|
194
|
+
it('matches flex-cli wording for key, scope, and schema-for options', () => {
|
|
195
|
+
const shareOutput = runCli('help search unset', 'sharetribe');
|
|
196
|
+
expect(shareOutput).toContain('key name');
|
|
197
|
+
expect(shareOutput).toContain('extended data scope (either metadata or public for listing schema,');
|
|
198
|
+
expect(shareOutput).toContain('metadata, private, protected or public for userProfile schema,');
|
|
199
|
+
expect(shareOutput).toContain('metadata or protected for transaction schema)');
|
|
200
|
+
expect(shareOutput).toContain('Subject of the schema (either listing, userProfile or transaction,');
|
|
201
|
+
expect(shareOutput).toContain('defaults to listing');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
176
205
|
describe('help notifications', () => {
|
|
177
206
|
it('has correct structure', () => {
|
|
178
207
|
const shareOutput = runCli('help notifications', 'sharetribe');
|