@rankwrangler/cli 0.2.2 → 0.2.3
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/README.md +15 -4
- package/dist/index.js +129 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ npm install -g @rankwrangler/cli
|
|
|
13
13
|
```bash
|
|
14
14
|
rw config set api-key rrk_...
|
|
15
15
|
rw products get B0DV53VS61
|
|
16
|
+
rw products history B0DV53VS61 --metrics bsr,price
|
|
16
17
|
# `rankwrangler` is also supported as an alias
|
|
17
18
|
```
|
|
18
19
|
|
|
@@ -24,6 +25,20 @@ bun run cli:build
|
|
|
24
25
|
|
|
25
26
|
## Release (Maintainers)
|
|
26
27
|
|
|
28
|
+
From repo root:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bun run release:bump patch
|
|
32
|
+
bun install
|
|
33
|
+
bun run release:collect-changelog-context
|
|
34
|
+
# draft CHANGELOG.md entry from commit context
|
|
35
|
+
bun run release:check
|
|
36
|
+
bun run cli:build
|
|
37
|
+
bun run release:check-cli-pack
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then publish:
|
|
41
|
+
|
|
27
42
|
```bash
|
|
28
43
|
cd packages/cli
|
|
29
44
|
set -a
|
|
@@ -32,7 +47,3 @@ set +a
|
|
|
32
47
|
npm whoami --userconfig ../../.npmrc
|
|
33
48
|
npm publish --access public --userconfig ../../.npmrc
|
|
34
49
|
```
|
|
35
|
-
|
|
36
|
-
If npm rejects the version as already published, bump `version` in
|
|
37
|
-
`packages/cli/package.json` to match the next `vX.Y.Z` release in `CHANGELOG.md`,
|
|
38
|
-
run `bun install` from repo root, and publish again.
|
package/dist/index.js
CHANGED
|
@@ -8,11 +8,13 @@ const ASIN_REGEX = /^[A-Z0-9]{10}$/i;
|
|
|
8
8
|
const TRAILING_SLASHES_REGEX = /\/+$/;
|
|
9
9
|
const API_SUFFIX_REGEX = /\/api$/i;
|
|
10
10
|
const DEFAULT_MARKETPLACE_ID = 'ATVPDKIKX0DER';
|
|
11
|
+
const HISTORY_METRIC_ALIASES = ['bsr', 'price'];
|
|
11
12
|
const CONFIG_DIR = path.join(homedir(), '.rankwrangler');
|
|
12
13
|
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
13
14
|
const DEFAULT_OUTPUT_PRETTY = true;
|
|
14
15
|
const SUPPORTED_COMMANDS = new Set([
|
|
15
16
|
'products:get',
|
|
17
|
+
'products:history',
|
|
16
18
|
'license:status',
|
|
17
19
|
'license:validate',
|
|
18
20
|
'config:show',
|
|
@@ -27,6 +29,11 @@ const { positionals, values } = parseArgs({
|
|
|
27
29
|
baseUrl: { type: 'string' },
|
|
28
30
|
marketplace: { type: 'string', short: 'm' },
|
|
29
31
|
asin: { type: 'string', multiple: true },
|
|
32
|
+
metrics: { type: 'string' },
|
|
33
|
+
startAt: { type: 'string' },
|
|
34
|
+
endAt: { type: 'string' },
|
|
35
|
+
days: { type: 'string' },
|
|
36
|
+
limit: { type: 'string' },
|
|
30
37
|
},
|
|
31
38
|
allowPositionals: true,
|
|
32
39
|
});
|
|
@@ -73,6 +80,9 @@ const runApiCommand = async (command, client, config) => {
|
|
|
73
80
|
}
|
|
74
81
|
return client.getProductInfoBatch.mutate({ marketplaceId, asins });
|
|
75
82
|
}
|
|
83
|
+
if (command.resource === 'products' && command.verb === 'history') {
|
|
84
|
+
return runProductHistoryCommand(command, client, config);
|
|
85
|
+
}
|
|
76
86
|
if (command.resource === 'license' && command.verb === 'status') {
|
|
77
87
|
return client.license.status.mutate();
|
|
78
88
|
}
|
|
@@ -84,6 +94,33 @@ const runApiCommand = async (command, client, config) => {
|
|
|
84
94
|
});
|
|
85
95
|
return null;
|
|
86
96
|
};
|
|
97
|
+
const runProductHistoryCommand = async (command, client, config) => {
|
|
98
|
+
const marketplaceId = requireMarketplaceId(config);
|
|
99
|
+
const asin = requireSingleAsin(command.args);
|
|
100
|
+
const metrics = resolveHistoryMetrics();
|
|
101
|
+
const historyWindow = resolveHistoryWindow();
|
|
102
|
+
const limit = parseIntegerOption({
|
|
103
|
+
value: values.limit,
|
|
104
|
+
optionName: 'limit',
|
|
105
|
+
min: 1,
|
|
106
|
+
max: 10000,
|
|
107
|
+
defaultValue: 5000,
|
|
108
|
+
});
|
|
109
|
+
const response = await client.getProductHistory.mutate({
|
|
110
|
+
marketplaceId,
|
|
111
|
+
asin,
|
|
112
|
+
metrics,
|
|
113
|
+
format: 'agent',
|
|
114
|
+
limit,
|
|
115
|
+
...historyWindow,
|
|
116
|
+
});
|
|
117
|
+
return buildCliHistoryResponse({
|
|
118
|
+
asin,
|
|
119
|
+
marketplaceId,
|
|
120
|
+
metrics,
|
|
121
|
+
response,
|
|
122
|
+
});
|
|
123
|
+
};
|
|
87
124
|
const runConfigCommand = async (command, config) => {
|
|
88
125
|
if (command.verb === 'show') {
|
|
89
126
|
return {
|
|
@@ -146,6 +183,13 @@ const requireAsins = (commandArgs) => {
|
|
|
146
183
|
}
|
|
147
184
|
return Array.from(new Set(candidates.map(normalizeAsin)));
|
|
148
185
|
};
|
|
186
|
+
const requireSingleAsin = (commandArgs) => {
|
|
187
|
+
const asins = requireAsins(commandArgs);
|
|
188
|
+
if (asins.length !== 1) {
|
|
189
|
+
fail('INVALID_INPUT', 'products history requires exactly one asin');
|
|
190
|
+
}
|
|
191
|
+
return asins[0];
|
|
192
|
+
};
|
|
149
193
|
const collectAsinCandidates = (commandArgs) => {
|
|
150
194
|
const optionAsins = [...(values.asin ?? [])];
|
|
151
195
|
const envAsins = process.env.RR_ASINS
|
|
@@ -163,6 +207,82 @@ const normalizeAsin = (value) => {
|
|
|
163
207
|
}
|
|
164
208
|
return normalized;
|
|
165
209
|
};
|
|
210
|
+
const resolveHistoryMetrics = () => {
|
|
211
|
+
const rawMetrics = values.metrics ?? process.env.RR_HISTORY_METRICS ?? 'bsr,price';
|
|
212
|
+
const requested = rawMetrics
|
|
213
|
+
.split(',')
|
|
214
|
+
.map(value => value.trim().toLowerCase())
|
|
215
|
+
.filter(Boolean);
|
|
216
|
+
if (requested.length === 0) {
|
|
217
|
+
fail('INVALID_INPUT', 'metrics cannot be empty');
|
|
218
|
+
}
|
|
219
|
+
const invalid = requested.filter(value => !HISTORY_METRIC_ALIASES.includes(value));
|
|
220
|
+
if (invalid.length > 0) {
|
|
221
|
+
fail('INVALID_INPUT', `unsupported history metric: ${invalid[0]}`, {
|
|
222
|
+
supportedMetrics: HISTORY_METRIC_ALIASES,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return Array.from(new Set(requested));
|
|
226
|
+
};
|
|
227
|
+
const resolveHistoryWindow = () => {
|
|
228
|
+
const startAt = values.startAt ? normalizeDateOption('startAt', values.startAt) : undefined;
|
|
229
|
+
const endAt = values.endAt ? normalizeDateOption('endAt', values.endAt) : undefined;
|
|
230
|
+
const days = parseIntegerOption({
|
|
231
|
+
value: values.days,
|
|
232
|
+
optionName: 'days',
|
|
233
|
+
min: 30,
|
|
234
|
+
max: 3650,
|
|
235
|
+
defaultValue: 365,
|
|
236
|
+
});
|
|
237
|
+
if (values.days && (startAt || endAt)) {
|
|
238
|
+
fail('INVALID_INPUT', 'use --days or --startAt/--endAt, not both');
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
...(startAt ? { startAt } : {}),
|
|
242
|
+
...(endAt ? { endAt } : {}),
|
|
243
|
+
days,
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
const parseIntegerOption = ({ value, optionName, min, max, defaultValue, }) => {
|
|
247
|
+
if (!value) {
|
|
248
|
+
return defaultValue;
|
|
249
|
+
}
|
|
250
|
+
const parsed = Number.parseInt(value, 10);
|
|
251
|
+
if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
|
|
252
|
+
fail('INVALID_INPUT', `${optionName} must be an integer between ${min} and ${max}`);
|
|
253
|
+
}
|
|
254
|
+
return parsed;
|
|
255
|
+
};
|
|
256
|
+
const normalizeDateOption = (optionName, value) => {
|
|
257
|
+
const parsed = new Date(value);
|
|
258
|
+
if (!Number.isFinite(parsed.getTime())) {
|
|
259
|
+
fail('INVALID_INPUT', `${optionName} must be a valid date`);
|
|
260
|
+
}
|
|
261
|
+
return parsed.toISOString();
|
|
262
|
+
};
|
|
263
|
+
const buildCliHistoryResponse = ({ asin, marketplaceId, metrics, response, }) => {
|
|
264
|
+
const series = {
|
|
265
|
+
...(metrics.includes('bsr') && response.series?.bsr ? { bsr: response.series.bsr } : {}),
|
|
266
|
+
...(metrics.includes('price') && response.series?.price ? { price: response.series.price } : {}),
|
|
267
|
+
};
|
|
268
|
+
const hasAnyPoints = Object.values(series).some(metricSeries => metricSeries.points.length > 0);
|
|
269
|
+
const status = normalizeHistoryStatus(response.status);
|
|
270
|
+
return {
|
|
271
|
+
schemaVersion: response.schemaVersion ?? 1,
|
|
272
|
+
asin,
|
|
273
|
+
marketplaceId,
|
|
274
|
+
status: status ?? (response.syncTriggered ? 'collecting' : hasAnyPoints ? 'ready' : 'empty'),
|
|
275
|
+
latestImportAt: response.latestImportAt,
|
|
276
|
+
syncTriggered: response.syncTriggered,
|
|
277
|
+
series,
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
const normalizeHistoryStatus = (status) => {
|
|
281
|
+
if (status === 'ready' || status === 'collecting' || status === 'empty') {
|
|
282
|
+
return status;
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
};
|
|
166
286
|
const resolveApiKey = (config) => {
|
|
167
287
|
return values.apiKey ?? config.apiKey ?? process.env.RR_LICENSE_KEY;
|
|
168
288
|
};
|
|
@@ -274,6 +394,7 @@ const printUsage = () => {
|
|
|
274
394
|
'',
|
|
275
395
|
'COMMANDS',
|
|
276
396
|
' products get <ASIN...>',
|
|
397
|
+
' products history <ASIN>',
|
|
277
398
|
' license status',
|
|
278
399
|
' license validate',
|
|
279
400
|
' config show',
|
|
@@ -288,6 +409,11 @@ const printUsage = () => {
|
|
|
288
409
|
' --baseUrl <origin> Override API origin',
|
|
289
410
|
` -m, --marketplace <id> Override marketplace (default: ${DEFAULT_MARKETPLACE_ID})`,
|
|
290
411
|
' --asin <ASIN> Add ASIN (repeatable)',
|
|
412
|
+
' --metrics <list> History metrics: bsr,price (default: bsr,price)',
|
|
413
|
+
' --days <N> History lookback window (30-3650, default: 365)',
|
|
414
|
+
' --startAt <ISO> History range start (ISO date/time)',
|
|
415
|
+
' --endAt <ISO> History range end (ISO date/time)',
|
|
416
|
+
' --limit <N> Max points per metric (1-10000, default: 5000)',
|
|
291
417
|
'',
|
|
292
418
|
'FILES',
|
|
293
419
|
` ${CONFIG_PATH} Local CLI config`,
|
|
@@ -298,10 +424,13 @@ const printUsage = () => {
|
|
|
298
424
|
' RR_MARKETPLACE_ID Marketplace fallback',
|
|
299
425
|
' RR_ASIN Single ASIN fallback',
|
|
300
426
|
' RR_ASINS Comma-separated ASIN fallback',
|
|
427
|
+
' RR_HISTORY_METRICS Comma-separated history metrics fallback',
|
|
301
428
|
'',
|
|
302
429
|
'EXAMPLES',
|
|
303
430
|
' rw config set api-key rrk_...',
|
|
304
431
|
' rw products get B0DV53VS61',
|
|
432
|
+
' rw products history B0DV53VS61 --metrics bsr,price',
|
|
433
|
+
' rw products history B0DV53VS61 --startAt 2025-01-01 --endAt 2025-12-31',
|
|
305
434
|
' rankwrangler license status',
|
|
306
435
|
];
|
|
307
436
|
console.log(usage.join('\n'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rankwrangler/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Official RankWrangler command line interface",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@rankwrangler/http-client": "^0.2.
|
|
23
|
+
"@rankwrangler/http-client": "^0.2.3"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/node": "^24.3.0",
|