@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.
Files changed (3) hide show
  1. package/README.md +15 -4
  2. package/dist/index.js +129 -0
  3. 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.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.2"
23
+ "@rankwrangler/http-client": "^0.2.3"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^24.3.0",