@rankwrangler/cli 0.5.0 → 0.8.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/README.md +15 -59
- package/dist/CHANGELOG.md +56 -1
- package/dist/cli-auth-error.d.ts +5 -0
- package/dist/cli-auth-error.js +10 -0
- package/dist/cli-auth.d.ts +36 -15
- package/dist/cli-auth.js +80 -37
- package/dist/cli-config-command.d.ts +83 -1
- package/dist/cli-config-command.js +72 -10
- package/dist/cli-config.d.ts +8 -1
- package/dist/cli-config.js +20 -2
- package/dist/cli-keyword-command.d.ts +9 -0
- package/dist/cli-keyword-command.js +69 -0
- package/dist/cli-options.d.ts +10 -6
- package/dist/cli-options.js +43 -23
- package/dist/cli-output.js +1 -1
- package/dist/cli-product-command.d.ts +10 -0
- package/dist/cli-product-command.js +62 -0
- package/dist/history-options.d.ts +4 -0
- package/dist/history-options.js +2 -0
- package/dist/index.js +34 -67
- package/dist/usage.js +34 -23
- package/package.json +9 -7
package/dist/cli-config.js
CHANGED
|
@@ -34,13 +34,31 @@ export const loadCliPathsOrDefault = async () => {
|
|
|
34
34
|
return getDefaultCliPaths();
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
|
-
export const
|
|
38
|
-
await
|
|
37
|
+
export const resetConfig = async (paths) => {
|
|
38
|
+
await Promise.all([
|
|
39
|
+
rm(paths.configPath, { force: true }),
|
|
40
|
+
rm(paths.globalConfigPath, { force: true }),
|
|
41
|
+
paths.storageDir === paths.defaultStorageDir
|
|
42
|
+
? Promise.resolve()
|
|
43
|
+
: rm(path.join(paths.defaultStorageDir, CONFIG_FILENAME), { force: true }),
|
|
44
|
+
]);
|
|
45
|
+
return {
|
|
46
|
+
paths: getDefaultCliPaths(),
|
|
47
|
+
config: {},
|
|
48
|
+
};
|
|
39
49
|
};
|
|
40
50
|
export const saveConfig = async (paths, config) => {
|
|
41
51
|
await mkdir(paths.storageDir, { recursive: true });
|
|
42
52
|
await writeFile(paths.configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
43
53
|
};
|
|
54
|
+
export const unsetStorageDir = async (paths) => {
|
|
55
|
+
await rm(paths.globalConfigPath, { force: true });
|
|
56
|
+
const nextPaths = getDefaultCliPaths();
|
|
57
|
+
return {
|
|
58
|
+
paths: nextPaths,
|
|
59
|
+
config: await loadConfig(nextPaths.configPath),
|
|
60
|
+
};
|
|
61
|
+
};
|
|
44
62
|
export const switchStorageDir = async ({ currentConfig, currentPaths, requestedStorageDir, }) => {
|
|
45
63
|
const storageDir = normalizeStorageDir(requestedStorageDir);
|
|
46
64
|
const nextPaths = buildCliPaths(storageDir);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { createRankWranglerClient } from '@rankwrangler/http-client';
|
|
2
|
+
import { type CliOptionValues } from './cli-options';
|
|
3
|
+
type CliFail = (code: string, message: string, details?: unknown) => never;
|
|
4
|
+
type CliClient = ReturnType<typeof createRankWranglerClient>;
|
|
5
|
+
export declare const runKeywordCommand: (command: {
|
|
6
|
+
verb: string;
|
|
7
|
+
args: string[];
|
|
8
|
+
}, client: CliClient, options: CliOptionValues, fail: CliFail) => Promise<unknown>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { parseIntegerOption } from './cli-options.js';
|
|
2
|
+
const KEYWORD_MARKETPLACE_ID = 'ATVPDKIKX0DER';
|
|
3
|
+
export const runKeywordCommand = async (command, client, options, fail) => {
|
|
4
|
+
if (options.refresh) {
|
|
5
|
+
fail('INVALID_INPUT', '--refresh is not supported for keyword reads');
|
|
6
|
+
}
|
|
7
|
+
if (command.verb === 'get') {
|
|
8
|
+
return await runKeywordGet(command.args, client, fail);
|
|
9
|
+
}
|
|
10
|
+
if (command.verb === 'search') {
|
|
11
|
+
return await runKeywordSearch(command.args, client, options, fail);
|
|
12
|
+
}
|
|
13
|
+
if (command.verb === 'history') {
|
|
14
|
+
return await runKeywordHistory(command.args, client, options, fail);
|
|
15
|
+
}
|
|
16
|
+
fail('UNKNOWN_COMMAND', 'Unknown Keyword command', {
|
|
17
|
+
command: `keyword ${command.verb}`,
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
const runKeywordGet = async (args, client, fail) => {
|
|
21
|
+
const keyword = requireKeyword(args, fail, 'keyword get');
|
|
22
|
+
return await client.keyword.get.query({
|
|
23
|
+
keyword,
|
|
24
|
+
marketplaceId: KEYWORD_MARKETPLACE_ID,
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
const runKeywordSearch = async (args, client, options, fail) => {
|
|
28
|
+
const text = requireKeyword(args, fail, 'keyword search');
|
|
29
|
+
return await client.keyword.search.query({
|
|
30
|
+
text,
|
|
31
|
+
marketplaceId: KEYWORD_MARKETPLACE_ID,
|
|
32
|
+
cursor: parseIntegerOption({
|
|
33
|
+
value: options.cursor,
|
|
34
|
+
optionName: 'cursor',
|
|
35
|
+
min: 0,
|
|
36
|
+
max: 1_000_000,
|
|
37
|
+
defaultValue: 0,
|
|
38
|
+
}, fail),
|
|
39
|
+
limit: parseIntegerOption({
|
|
40
|
+
value: options.limit,
|
|
41
|
+
optionName: 'limit',
|
|
42
|
+
min: 1,
|
|
43
|
+
max: 100,
|
|
44
|
+
defaultValue: 25,
|
|
45
|
+
}, fail),
|
|
46
|
+
merchOnly: true,
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
const runKeywordHistory = async (args, client, options, fail) => {
|
|
50
|
+
const keyword = requireKeyword(args, fail, 'keyword history');
|
|
51
|
+
return await client.keyword.history.query({
|
|
52
|
+
keyword,
|
|
53
|
+
marketplaceId: KEYWORD_MARKETPLACE_ID,
|
|
54
|
+
rangeDays: parseIntegerOption({
|
|
55
|
+
value: options.rangeDays ?? options.days,
|
|
56
|
+
optionName: 'rangeDays',
|
|
57
|
+
min: 7,
|
|
58
|
+
max: 365,
|
|
59
|
+
defaultValue: 90,
|
|
60
|
+
}, fail),
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
const requireKeyword = (args, fail, commandName) => {
|
|
64
|
+
const keyword = args.join(' ').trim();
|
|
65
|
+
if (!keyword) {
|
|
66
|
+
fail('INVALID_INPUT', `${commandName} requires text`);
|
|
67
|
+
}
|
|
68
|
+
return keyword;
|
|
69
|
+
};
|
package/dist/cli-options.d.ts
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import type { CliConfig } from './cli-config';
|
|
2
|
-
import { type HistoryMetricAlias } from './history-
|
|
2
|
+
import { type HistoryBucket, type HistoryMetricAlias } from './history-options';
|
|
3
3
|
type CliFail = (code: string, message: string, details?: unknown) => never;
|
|
4
|
-
export
|
|
4
|
+
export interface CliOptionValues {
|
|
5
5
|
baseUrl?: string;
|
|
6
6
|
marketplace?: string;
|
|
7
|
-
asin?: string[];
|
|
8
7
|
metrics?: string;
|
|
9
8
|
startAt?: string;
|
|
10
9
|
endAt?: string;
|
|
11
10
|
days?: string;
|
|
11
|
+
rangeDays?: string;
|
|
12
12
|
limit?: string;
|
|
13
|
+
bucket?: string;
|
|
14
|
+
cursor?: string;
|
|
15
|
+
refresh?: boolean;
|
|
13
16
|
help?: boolean;
|
|
14
17
|
version?: boolean;
|
|
15
|
-
}
|
|
18
|
+
}
|
|
16
19
|
export declare const requireMarketplaceId: (values: CliOptionValues, config: CliConfig) => string;
|
|
17
|
-
export declare const
|
|
18
|
-
export declare const
|
|
20
|
+
export declare const requireSingleAsin: (commandArgs: string[], fail: CliFail, commandName?: string) => string;
|
|
21
|
+
export declare const requireAsins: (commandArgs: string[], fail: CliFail, commandName?: string) => string[];
|
|
19
22
|
export declare const resolveHistoryMetrics: (values: CliOptionValues, fail: CliFail) => HistoryMetricAlias[];
|
|
23
|
+
export declare const resolveHistoryBucket: (values: CliOptionValues, fail: CliFail) => HistoryBucket;
|
|
20
24
|
export declare const resolveHistoryWindow: (values: CliOptionValues, fail: CliFail) => {
|
|
21
25
|
days: number;
|
|
22
26
|
endAt?: string | undefined;
|
package/dist/cli-options.js
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
|
-
import { HISTORY_METRIC_ALIASES } from './history-
|
|
1
|
+
import { HISTORY_BUCKETS, HISTORY_METRIC_ALIASES, } from './history-options.js';
|
|
2
2
|
const ASIN_REGEX = /^[A-Z0-9]{10}$/i;
|
|
3
3
|
const TRAILING_SLASHES_REGEX = /\/+$/;
|
|
4
4
|
const API_SUFFIX_REGEX = /\/api$/i;
|
|
5
5
|
const DEFAULT_MARKETPLACE_ID = 'ATVPDKIKX0DER';
|
|
6
6
|
export const requireMarketplaceId = (values, config) => {
|
|
7
|
-
return values.marketplace ??
|
|
7
|
+
return (values.marketplace ??
|
|
8
|
+
config.marketplaceId ??
|
|
9
|
+
process.env.RR_MARKETPLACE_ID ??
|
|
10
|
+
DEFAULT_MARKETPLACE_ID);
|
|
8
11
|
};
|
|
9
|
-
export const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
.filter(Boolean);
|
|
13
|
-
if (candidates.length === 0) {
|
|
14
|
-
fail('INVALID_INPUT', 'at least one asin is required');
|
|
12
|
+
export const requireSingleAsin = (commandArgs, fail, commandName = 'product history') => {
|
|
13
|
+
if (commandArgs.length !== 1) {
|
|
14
|
+
fail('INVALID_INPUT', `${commandName} requires exactly one ASIN`);
|
|
15
15
|
}
|
|
16
|
-
return
|
|
16
|
+
return normalizeAsin(commandArgs[0] ?? '', fail);
|
|
17
17
|
};
|
|
18
|
-
export const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
export const requireAsins = (commandArgs, fail, commandName = 'product get') => {
|
|
19
|
+
if (commandArgs.length < 1 || commandArgs.length > 200) {
|
|
20
|
+
fail('INVALID_INPUT', `${commandName} requires between 1 and 200 ASINs`);
|
|
21
|
+
}
|
|
22
|
+
const asins = commandArgs.map(value => normalizeAsin(value, fail));
|
|
23
|
+
if (new Set(asins).size !== asins.length) {
|
|
24
|
+
fail('INVALID_INPUT', `${commandName} requires unique ASINs`);
|
|
22
25
|
}
|
|
23
|
-
return asins
|
|
26
|
+
return asins;
|
|
24
27
|
};
|
|
25
28
|
export const resolveHistoryMetrics = (values, fail) => {
|
|
26
|
-
const requested = (values.metrics ?? process.env.RR_HISTORY_METRICS ?? '
|
|
29
|
+
const requested = (values.metrics ?? process.env.RR_HISTORY_METRICS ?? 'salesRank,price')
|
|
27
30
|
.split(',')
|
|
28
|
-
.map(value => value.trim()
|
|
31
|
+
.map(value => normalizeHistoryMetric(value.trim()))
|
|
29
32
|
.filter(Boolean);
|
|
30
33
|
if (requested.length === 0) {
|
|
31
34
|
fail('INVALID_INPUT', 'metrics cannot be empty');
|
|
@@ -38,8 +41,19 @@ export const resolveHistoryMetrics = (values, fail) => {
|
|
|
38
41
|
}
|
|
39
42
|
return Array.from(new Set(requested));
|
|
40
43
|
};
|
|
44
|
+
export const resolveHistoryBucket = (values, fail) => {
|
|
45
|
+
const requested = (values.bucket ?? 'auto').trim().toLowerCase();
|
|
46
|
+
if (!HISTORY_BUCKETS.includes(requested)) {
|
|
47
|
+
fail('INVALID_INPUT', `unsupported history bucket: ${requested}`, {
|
|
48
|
+
supportedBuckets: HISTORY_BUCKETS,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return requested;
|
|
52
|
+
};
|
|
41
53
|
export const resolveHistoryWindow = (values, fail) => {
|
|
42
|
-
const startAt = values.startAt
|
|
54
|
+
const startAt = values.startAt
|
|
55
|
+
? normalizeDateOption('startAt', values.startAt, fail)
|
|
56
|
+
: undefined;
|
|
43
57
|
const endAt = values.endAt ? normalizeDateOption('endAt', values.endAt, fail) : undefined;
|
|
44
58
|
if (values.days && (startAt || endAt)) {
|
|
45
59
|
fail('INVALID_INPUT', 'use --days or --startAt/--endAt, not both');
|
|
@@ -60,7 +74,10 @@ export const resolveBaseUrl = (values, config, defaultBaseUrl, fail) => {
|
|
|
60
74
|
return normalizeBaseUrl(values.baseUrl ?? config.baseUrl ?? process.env.RR_API_URL ?? defaultBaseUrl, fail);
|
|
61
75
|
};
|
|
62
76
|
export const normalizeBaseUrl = (value, fail) => {
|
|
63
|
-
const normalized = value
|
|
77
|
+
const normalized = value
|
|
78
|
+
.trim()
|
|
79
|
+
.replace(TRAILING_SLASHES_REGEX, '')
|
|
80
|
+
.replace(API_SUFFIX_REGEX, '');
|
|
64
81
|
if (!normalized) {
|
|
65
82
|
fail('INVALID_INPUT', 'base url cannot be empty');
|
|
66
83
|
}
|
|
@@ -90,9 +107,12 @@ const normalizeAsin = (value, fail) => {
|
|
|
90
107
|
}
|
|
91
108
|
return normalized;
|
|
92
109
|
};
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
const normalizeHistoryMetric = (value) => {
|
|
111
|
+
if (value.toLowerCase() === 'salesrank') {
|
|
112
|
+
return 'salesRank';
|
|
113
|
+
}
|
|
114
|
+
if (value.toLowerCase() === 'price') {
|
|
115
|
+
return 'price';
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
98
118
|
};
|
package/dist/cli-output.js
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { createRankWranglerClient } from '@rankwrangler/http-client';
|
|
2
|
+
import type { CliConfig } from './cli-config';
|
|
3
|
+
import { type CliOptionValues } from './cli-options';
|
|
4
|
+
type CliFail = (code: string, message: string, details?: unknown) => never;
|
|
5
|
+
type CliClient = ReturnType<typeof createRankWranglerClient>;
|
|
6
|
+
export declare const runProductCommand: (command: {
|
|
7
|
+
verb: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
}, client: CliClient, config: CliConfig, options: CliOptionValues, fail: CliFail) => Promise<unknown>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { parseIntegerOption, requireAsins, requireMarketplaceId, requireSingleAsin, resolveHistoryBucket, resolveHistoryMetrics, resolveHistoryWindow, } from './cli-options.js';
|
|
2
|
+
export const runProductCommand = async (command, client, config, options, fail) => {
|
|
3
|
+
if (command.verb !== 'search' && options.refresh) {
|
|
4
|
+
fail('INVALID_INPUT', '--refresh is only supported for product search');
|
|
5
|
+
}
|
|
6
|
+
if (command.verb === 'get') {
|
|
7
|
+
return await runProductGet(command.args, client, config, options, fail);
|
|
8
|
+
}
|
|
9
|
+
if (command.verb === 'search') {
|
|
10
|
+
return await runProductSearch(command.args, client, options, fail);
|
|
11
|
+
}
|
|
12
|
+
if (command.verb === 'history') {
|
|
13
|
+
return await runProductHistory(command.args, client, config, options, fail);
|
|
14
|
+
}
|
|
15
|
+
fail('UNKNOWN_COMMAND', 'Unknown Product command', {
|
|
16
|
+
command: `product ${command.verb}`,
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
const runProductGet = async (args, client, config, options, fail) => {
|
|
20
|
+
const marketplaceId = requireMarketplaceId(options, config);
|
|
21
|
+
const asins = requireAsins(args, fail);
|
|
22
|
+
if (asins.length === 1) {
|
|
23
|
+
return await client.product.get.mutate({
|
|
24
|
+
marketplaceId,
|
|
25
|
+
asin: asins[0],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return await client.product.getMany.mutate({
|
|
29
|
+
products: asins.map(asin => ({ marketplaceId, asin })),
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
const runProductSearch = async (args, client, options, fail) => {
|
|
33
|
+
const term = args.join(' ').trim();
|
|
34
|
+
if (!term) {
|
|
35
|
+
fail('INVALID_INPUT', 'product search requires a keyword');
|
|
36
|
+
}
|
|
37
|
+
return await client.product.search.mutate({
|
|
38
|
+
term,
|
|
39
|
+
refresh: Boolean(options.refresh),
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
const runProductHistory = async (args, client, config, options, fail) => {
|
|
43
|
+
const asin = requireSingleAsin(args, fail, 'product history');
|
|
44
|
+
const historyOptions = resolveHistoryOptions(options, fail);
|
|
45
|
+
return await client.product.history.mutate({
|
|
46
|
+
marketplaceId: requireMarketplaceId(options, config),
|
|
47
|
+
asin,
|
|
48
|
+
...historyOptions,
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const resolveHistoryOptions = (options, fail) => ({
|
|
52
|
+
metrics: resolveHistoryMetrics(options, fail),
|
|
53
|
+
bucket: resolveHistoryBucket(options, fail),
|
|
54
|
+
limit: parseIntegerOption({
|
|
55
|
+
value: options.limit,
|
|
56
|
+
optionName: 'limit',
|
|
57
|
+
min: 1,
|
|
58
|
+
max: 10_000,
|
|
59
|
+
defaultValue: 5000,
|
|
60
|
+
}, fail),
|
|
61
|
+
...resolveHistoryWindow(options, fail),
|
|
62
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const HISTORY_METRIC_ALIASES: readonly ["salesRank", "price"];
|
|
2
|
+
export declare const HISTORY_BUCKETS: readonly ["auto", "day", "week", "month"];
|
|
3
|
+
export type HistoryMetricAlias = (typeof HISTORY_METRIC_ALIASES)[number];
|
|
4
|
+
export type HistoryBucket = (typeof HISTORY_BUCKETS)[number];
|
package/dist/index.js
CHANGED
|
@@ -2,23 +2,28 @@
|
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
3
|
import { createRankWranglerClient, DEFAULT_API_BASE_URL } from '@rankwrangler/http-client';
|
|
4
4
|
import { resolveApiKey, runAuthCommand } from './cli-auth.js';
|
|
5
|
-
import { printBundledChangelog, printCliVersion } from './cli-metadata.js';
|
|
6
|
-
import { runConfigCommand } from './cli-config-command.js';
|
|
7
5
|
import { loadCliContext, loadCliPathsOrDefault } from './cli-config.js';
|
|
8
|
-
import {
|
|
6
|
+
import { runConfigCommand } from './cli-config-command.js';
|
|
7
|
+
import { runKeywordCommand } from './cli-keyword-command.js';
|
|
8
|
+
import { printBundledChangelog, printCliVersion } from './cli-metadata.js';
|
|
9
|
+
import { resolveBaseUrl } from './cli-options.js';
|
|
9
10
|
import { fail, printSuccess, resolveError } from './cli-output.js';
|
|
10
|
-
import {
|
|
11
|
+
import { runProductCommand } from './cli-product-command.js';
|
|
11
12
|
import { printUsage } from './usage.js';
|
|
12
13
|
const SUPPORTED_COMMANDS = new Set([
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
14
|
+
'product:get',
|
|
15
|
+
'product:search',
|
|
16
|
+
'product:history',
|
|
17
|
+
'keyword:get',
|
|
18
|
+
'keyword:search',
|
|
19
|
+
'keyword:history',
|
|
17
20
|
'auth:status',
|
|
18
21
|
'auth:set',
|
|
19
22
|
'auth:clear',
|
|
20
23
|
'config:show',
|
|
21
|
-
'config:
|
|
24
|
+
'config:get',
|
|
25
|
+
'config:unset',
|
|
26
|
+
'config:reset',
|
|
22
27
|
'config:set',
|
|
23
28
|
]);
|
|
24
29
|
const { positionals, values } = parseArgs({
|
|
@@ -28,12 +33,16 @@ const { positionals, values } = parseArgs({
|
|
|
28
33
|
version: { type: 'boolean' },
|
|
29
34
|
baseUrl: { type: 'string' },
|
|
30
35
|
marketplace: { type: 'string', short: 'm' },
|
|
31
|
-
asin: { type: 'string', multiple: true },
|
|
32
36
|
metrics: { type: 'string' },
|
|
33
37
|
startAt: { type: 'string' },
|
|
34
38
|
endAt: { type: 'string' },
|
|
35
39
|
days: { type: 'string' },
|
|
40
|
+
rangeDays: { type: 'string' },
|
|
36
41
|
limit: { type: 'string' },
|
|
42
|
+
bucket: { type: 'string' },
|
|
43
|
+
cursor: { type: 'string' },
|
|
44
|
+
refresh: { type: 'boolean' },
|
|
45
|
+
stdin: { type: 'boolean' },
|
|
37
46
|
},
|
|
38
47
|
allowPositionals: true,
|
|
39
48
|
});
|
|
@@ -48,11 +57,9 @@ const main = async () => {
|
|
|
48
57
|
process.exit(values.help ? 0 : 1);
|
|
49
58
|
}
|
|
50
59
|
const metaCommand = resolveMetaCommand(positionals);
|
|
51
|
-
if (metaCommand) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
60
|
+
if (metaCommand?.name === 'changelog') {
|
|
61
|
+
printBundledChangelog();
|
|
62
|
+
return;
|
|
56
63
|
}
|
|
57
64
|
const command = resolveCommandOrFail(positionals);
|
|
58
65
|
if (!isSupportedCommand(command)) {
|
|
@@ -66,70 +73,32 @@ const main = async () => {
|
|
|
66
73
|
return;
|
|
67
74
|
}
|
|
68
75
|
if (command.resource === 'auth') {
|
|
69
|
-
printSuccess(await runAuthCommand(command, fail
|
|
76
|
+
printSuccess(await runAuthCommand(command, fail, {
|
|
77
|
+
stdin: Boolean(values.stdin),
|
|
78
|
+
}));
|
|
70
79
|
return;
|
|
71
80
|
}
|
|
72
81
|
const apiKey = await resolveApiKey();
|
|
73
82
|
if (!apiKey) {
|
|
74
|
-
fail('MISSING_CONFIG', '
|
|
83
|
+
fail('MISSING_CONFIG', 'Merchbase API key is required. run `rw auth set`, `rw auth set --stdin`, or set MERCHBASE_API_KEY');
|
|
75
84
|
}
|
|
76
85
|
const client = createRankWranglerClient({
|
|
77
86
|
baseUrl: resolveBaseUrl(optionValues, config, DEFAULT_API_BASE_URL, fail),
|
|
78
87
|
apiKey,
|
|
79
88
|
});
|
|
80
|
-
printSuccess(await runApiCommand(command, client, config));
|
|
89
|
+
printSuccess(await runApiCommand(command, client, config, optionValues));
|
|
81
90
|
};
|
|
82
|
-
const runApiCommand = async (command, client, config) => {
|
|
83
|
-
if (command.resource === '
|
|
84
|
-
|
|
85
|
-
const marketplaceId = requireMarketplaceId(optionValues, config);
|
|
86
|
-
const asins = requireAsins(command.args, optionValues, fail);
|
|
87
|
-
if (asins.length === 1) {
|
|
88
|
-
return client.getProductInfo.mutate({ marketplaceId, asin: asins[0] });
|
|
89
|
-
}
|
|
90
|
-
return client.getProductInfoBatch.mutate({ marketplaceId, asins });
|
|
91
|
-
}
|
|
92
|
-
if (command.resource === 'products' && command.verb === 'history') {
|
|
93
|
-
return runProductHistoryCommand(command, client, config);
|
|
91
|
+
const runApiCommand = async (command, client, config, options) => {
|
|
92
|
+
if (command.resource === 'product') {
|
|
93
|
+
return await runProductCommand(command, client, config, options, fail);
|
|
94
94
|
}
|
|
95
|
-
if (command.resource === '
|
|
96
|
-
return client
|
|
97
|
-
}
|
|
98
|
-
if (command.resource === 'license' && command.verb === 'validate') {
|
|
99
|
-
return client.license.validate.mutate();
|
|
95
|
+
if (command.resource === 'keyword') {
|
|
96
|
+
return await runKeywordCommand(command, client, options, fail);
|
|
100
97
|
}
|
|
101
98
|
fail('UNKNOWN_COMMAND', 'Unknown command', {
|
|
102
99
|
command: `${command.resource} ${command.verb}`,
|
|
103
100
|
});
|
|
104
101
|
};
|
|
105
|
-
const runProductHistoryCommand = async (command, client, config) => {
|
|
106
|
-
const optionValues = values;
|
|
107
|
-
const asin = requireSingleAsin(command.args, optionValues, fail);
|
|
108
|
-
const marketplaceId = requireMarketplaceId(optionValues, config);
|
|
109
|
-
const metrics = resolveHistoryMetrics(optionValues, fail);
|
|
110
|
-
const historyWindow = resolveHistoryWindow(optionValues, fail);
|
|
111
|
-
const limit = parseIntegerOption({
|
|
112
|
-
value: optionValues.limit,
|
|
113
|
-
optionName: 'limit',
|
|
114
|
-
min: 1,
|
|
115
|
-
max: 10000,
|
|
116
|
-
defaultValue: 5000,
|
|
117
|
-
}, fail);
|
|
118
|
-
const response = await client.getProductHistory.mutate({
|
|
119
|
-
marketplaceId,
|
|
120
|
-
asin,
|
|
121
|
-
metrics,
|
|
122
|
-
format: 'agent',
|
|
123
|
-
limit,
|
|
124
|
-
...historyWindow,
|
|
125
|
-
});
|
|
126
|
-
return buildCliHistoryResponse({
|
|
127
|
-
asin,
|
|
128
|
-
marketplaceId,
|
|
129
|
-
metrics,
|
|
130
|
-
response: response,
|
|
131
|
-
});
|
|
132
|
-
};
|
|
133
102
|
const resolveMetaCommand = (inputPositionals) => {
|
|
134
103
|
if (inputPositionals.length === 1 && inputPositionals[0] === 'changelog') {
|
|
135
104
|
return { name: 'changelog' };
|
|
@@ -138,7 +107,7 @@ const resolveMetaCommand = (inputPositionals) => {
|
|
|
138
107
|
};
|
|
139
108
|
const resolveCommand = (inputPositionals) => {
|
|
140
109
|
const [first, second, ...rest] = inputPositionals;
|
|
141
|
-
if (!first
|
|
110
|
+
if (!(first && second)) {
|
|
142
111
|
return null;
|
|
143
112
|
}
|
|
144
113
|
return {
|
|
@@ -155,9 +124,7 @@ const resolveCommandOrFail = (inputPositionals) => {
|
|
|
155
124
|
fail('UNKNOWN_COMMAND', 'Unknown command', { command: inputPositionals.join(' ') });
|
|
156
125
|
throw new Error('unreachable');
|
|
157
126
|
};
|
|
158
|
-
const isSupportedCommand = (command) => {
|
|
159
|
-
return SUPPORTED_COMMANDS.has(`${command.resource}:${command.verb}`);
|
|
160
|
-
};
|
|
127
|
+
const isSupportedCommand = (command) => SUPPORTED_COMMANDS.has(`${command.resource}:${command.verb}`);
|
|
161
128
|
await main().catch(error => {
|
|
162
129
|
const resolved = resolveError(error);
|
|
163
130
|
fail(resolved.code, resolved.message, resolved.details);
|
package/dist/usage.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { API_KEY_ENV_VAR } from './cli-auth.js';
|
|
2
2
|
const DEFAULT_MARKETPLACE_ID = 'ATVPDKIKX0DER';
|
|
3
3
|
export const printUsage = (paths) => {
|
|
4
4
|
const usage = [
|
|
@@ -15,54 +15,65 @@ export const printUsage = (paths) => {
|
|
|
15
15
|
'',
|
|
16
16
|
'COMMANDS',
|
|
17
17
|
' changelog',
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
18
|
+
' product get <ASIN...>',
|
|
19
|
+
' product search <keyword>',
|
|
20
|
+
' product history <ASIN>',
|
|
21
|
+
' keyword get <keyword>',
|
|
22
|
+
' keyword search <text>',
|
|
23
|
+
' keyword history <keyword>',
|
|
22
24
|
' auth status',
|
|
23
|
-
' auth set
|
|
25
|
+
' auth set [apiKey]',
|
|
26
|
+
' auth set --stdin',
|
|
24
27
|
' auth clear',
|
|
25
28
|
' config show',
|
|
26
|
-
' config
|
|
29
|
+
' config get <key>',
|
|
27
30
|
' config set base-url <origin>',
|
|
28
31
|
' config set marketplace <marketplaceId>',
|
|
29
32
|
' config set storage-dir <path>',
|
|
33
|
+
' config unset <key>',
|
|
34
|
+
' config reset',
|
|
30
35
|
'',
|
|
31
36
|
'OPTIONS',
|
|
32
37
|
' -h, --help Show this help message',
|
|
33
38
|
' --version Print the CLI version',
|
|
34
39
|
' --baseUrl <origin> Override API origin',
|
|
35
40
|
` -m, --marketplace <id> Override marketplace (default: ${DEFAULT_MARKETPLACE_ID})`,
|
|
36
|
-
' --
|
|
37
|
-
' --metrics <list>
|
|
38
|
-
' --
|
|
39
|
-
' --
|
|
40
|
-
' --
|
|
41
|
-
' --
|
|
41
|
+
' --refresh Product search refresh (separate Search contract)',
|
|
42
|
+
' --metrics <list> Product history metrics: salesRank,price (default: salesRank,price)',
|
|
43
|
+
' --bucket <unit> Product history buckets: auto,day,week,month (default: auto)',
|
|
44
|
+
' --days <N> Product history lookback (30-3650, default: 365)',
|
|
45
|
+
' --rangeDays <N> Keyword history lookback (7-365, default: 90)',
|
|
46
|
+
' --startAt <ISO> Product history range start (ISO date/time)',
|
|
47
|
+
' --endAt <ISO> Product history range end (ISO date/time)',
|
|
48
|
+
' --limit <N> Result limit',
|
|
49
|
+
' --cursor <N> Keyword search offset (default: 0)',
|
|
50
|
+
' --stdin Read auth secret from standard input',
|
|
42
51
|
'',
|
|
43
52
|
'FILES',
|
|
44
53
|
` ${paths.configPath} Active CLI config`,
|
|
45
54
|
` ${paths.globalConfigPath} Global CLI settings`,
|
|
46
55
|
'',
|
|
47
56
|
'ENVIRONMENT',
|
|
48
|
-
` ${
|
|
57
|
+
` ${API_KEY_ENV_VAR} Merchbase API key override for CI/automation`,
|
|
49
58
|
' RR_API_URL API origin fallback',
|
|
50
59
|
' RR_MARKETPLACE_ID Marketplace fallback',
|
|
51
60
|
' RR_STORAGE_DIR Storage directory override',
|
|
52
|
-
'
|
|
53
|
-
' RR_ASINS Comma-separated ASIN fallback',
|
|
54
|
-
' RR_HISTORY_METRICS Comma-separated history metrics fallback',
|
|
61
|
+
' RR_HISTORY_METRICS Comma-separated product history metrics fallback',
|
|
55
62
|
'',
|
|
56
63
|
'EXAMPLES',
|
|
57
|
-
' rw auth set
|
|
58
|
-
|
|
64
|
+
' rw auth set ak_...',
|
|
65
|
+
' printf %s "$MERCHBASE_API_KEY" | rw auth set --stdin',
|
|
66
|
+
` ${API_KEY_ENV_VAR}=ak_... rw product get B0DV53VS61`,
|
|
67
|
+
' rw product get B0DV53VS61 B012345678',
|
|
68
|
+
' rw product search "retro gardening shirt"',
|
|
69
|
+
' rw product history B0DV53VS61 --bucket week',
|
|
70
|
+
' rw keyword get "retro gardening shirt"',
|
|
71
|
+
' rw keyword search "retro gardening" --limit 10',
|
|
72
|
+
' rw keyword history "retro gardening shirt" --rangeDays 90',
|
|
59
73
|
' RR_STORAGE_DIR=~/.config/rankwrangler rw config show',
|
|
60
74
|
' rw --version',
|
|
61
75
|
' rw changelog',
|
|
62
|
-
'
|
|
63
|
-
' rw products history B0DV53VS61 --metrics bsr,price',
|
|
64
|
-
' rw products history B0DV53VS61 --startAt 2025-01-01 --endAt 2025-12-31',
|
|
65
|
-
' rankwrangler license status',
|
|
76
|
+
' rankwrangler auth status',
|
|
66
77
|
];
|
|
67
78
|
console.log(usage.join('\n'));
|
|
68
79
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rankwrangler/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Official RankWrangler command line interface",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,19 +16,21 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsc -p tsconfig.json && node scripts/fix-dist-imports.mjs && node scripts/sync-changelog.mjs",
|
|
18
18
|
"prepack": "node scripts/sync-changelog.mjs",
|
|
19
|
-
"test:e2e": "bun test test
|
|
19
|
+
"test:e2e": "bun test test/*.e2e.test.ts",
|
|
20
20
|
"prepublishOnly": "node ../../scripts/release/ensure-cli-http-client-published.mjs"
|
|
21
21
|
},
|
|
22
22
|
"publishConfig": {
|
|
23
|
-
"access": "public"
|
|
23
|
+
"access": "public",
|
|
24
|
+
"provenance": true
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"@
|
|
27
|
-
"
|
|
27
|
+
"@merchbaseco/access": "0.3.0",
|
|
28
|
+
"@rankwrangler/http-client": "^0.8.0",
|
|
29
|
+
"keytar": "7.9.0"
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|
|
30
|
-
"@types/node": "
|
|
31
|
-
"typescript": "
|
|
32
|
+
"@types/node": "24.10.12",
|
|
33
|
+
"typescript": "5.9.3"
|
|
32
34
|
},
|
|
33
35
|
"packageManager": "bun@1.1.27"
|
|
34
36
|
}
|