@rankwrangler/cli 0.2.2 → 0.4.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 +23 -5
- package/dist/cli-config.d.ts +31 -0
- package/dist/cli-config.js +136 -0
- package/dist/cli-options.d.ts +34 -0
- package/dist/cli-options.js +101 -0
- package/dist/cli-output.d.ts +11 -0
- package/dist/cli-output.js +53 -0
- package/dist/history-response.d.ts +41 -0
- package/dist/history-response.js +26 -0
- package/dist/index.js +107 -229
- package/dist/usage.d.ts +2 -0
- package/dist/usage.js +55 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -11,19 +11,41 @@ npm install -g @rankwrangler/cli
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
14
|
+
export RR_LICENSE_KEY=rrk_...
|
|
15
|
+
rw config set storage-dir ~/.config/rankwrangler
|
|
15
16
|
rw products get B0DV53VS61
|
|
17
|
+
rw products history B0DV53VS61 --metrics bsr,price
|
|
16
18
|
# `rankwrangler` is also supported as an alias
|
|
17
19
|
```
|
|
18
20
|
|
|
21
|
+
`RR_LICENSE_KEY` is the API-key source. `rw config set storage-dir <path>` saves the
|
|
22
|
+
active storage directory globally. After that, every CLI command reads and writes its non-secret
|
|
23
|
+
config/data from that directory, while preserving existing config values when switching.
|
|
24
|
+
`RR_STORAGE_DIR`, `RR_API_URL`, and `RR_MARKETPLACE_ID` override saved CLI config when set.
|
|
25
|
+
|
|
19
26
|
## Development
|
|
20
27
|
|
|
21
28
|
```bash
|
|
22
29
|
bun run cli:build
|
|
30
|
+
bun run cli:test:e2e
|
|
23
31
|
```
|
|
24
32
|
|
|
25
33
|
## Release (Maintainers)
|
|
26
34
|
|
|
35
|
+
From repo root:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
bun run release:bump patch
|
|
39
|
+
bun install
|
|
40
|
+
bun run release:collect-changelog-context
|
|
41
|
+
# draft CHANGELOG.md entry from commit context
|
|
42
|
+
bun run release:check
|
|
43
|
+
bun run cli:build
|
|
44
|
+
bun run release:check-cli-pack
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Then publish:
|
|
48
|
+
|
|
27
49
|
```bash
|
|
28
50
|
cd packages/cli
|
|
29
51
|
set -a
|
|
@@ -32,7 +54,3 @@ set +a
|
|
|
32
54
|
npm whoami --userconfig ../../.npmrc
|
|
33
55
|
npm publish --access public --userconfig ../../.npmrc
|
|
34
56
|
```
|
|
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.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type CliConfig = {
|
|
2
|
+
baseUrl?: string;
|
|
3
|
+
marketplaceId?: string;
|
|
4
|
+
};
|
|
5
|
+
export type CliPaths = {
|
|
6
|
+
defaultStorageDir: string;
|
|
7
|
+
storageDir: string;
|
|
8
|
+
configPath: string;
|
|
9
|
+
globalConfigPath: string;
|
|
10
|
+
};
|
|
11
|
+
export declare class CliStorageError extends Error {
|
|
12
|
+
readonly code: string;
|
|
13
|
+
readonly details?: unknown;
|
|
14
|
+
constructor(code: string, message: string, details?: unknown);
|
|
15
|
+
}
|
|
16
|
+
export declare const getDefaultCliPaths: () => CliPaths;
|
|
17
|
+
export declare const loadCliContext: () => Promise<{
|
|
18
|
+
paths: CliPaths;
|
|
19
|
+
config: CliConfig;
|
|
20
|
+
}>;
|
|
21
|
+
export declare const loadCliPathsOrDefault: () => Promise<CliPaths>;
|
|
22
|
+
export declare const clearConfig: (paths: CliPaths) => Promise<void>;
|
|
23
|
+
export declare const saveConfig: (paths: CliPaths, config: CliConfig) => Promise<void>;
|
|
24
|
+
export declare const switchStorageDir: ({ currentConfig, currentPaths, requestedStorageDir, }: {
|
|
25
|
+
currentConfig: CliConfig;
|
|
26
|
+
currentPaths: CliPaths;
|
|
27
|
+
requestedStorageDir: string;
|
|
28
|
+
}) => Promise<{
|
|
29
|
+
paths: CliPaths;
|
|
30
|
+
config: CliConfig;
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const DEFAULT_STORAGE_DIR = path.join(homedir(), '.rankwrangler');
|
|
5
|
+
const CONFIG_FILENAME = 'config.json';
|
|
6
|
+
const GLOBAL_CONFIG_FILENAME = 'global.json';
|
|
7
|
+
const STORAGE_DIR_ENV_VAR = 'RR_STORAGE_DIR';
|
|
8
|
+
export class CliStorageError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
details;
|
|
11
|
+
constructor(code, message, details) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'CliStorageError';
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.details = details;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export const getDefaultCliPaths = () => {
|
|
19
|
+
return buildCliPaths(DEFAULT_STORAGE_DIR);
|
|
20
|
+
};
|
|
21
|
+
export const loadCliContext = async () => {
|
|
22
|
+
const paths = await resolveCliPaths();
|
|
23
|
+
const config = await loadConfig(paths.configPath);
|
|
24
|
+
return {
|
|
25
|
+
paths,
|
|
26
|
+
config,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export const loadCliPathsOrDefault = async () => {
|
|
30
|
+
try {
|
|
31
|
+
return await resolveCliPaths();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return getDefaultCliPaths();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
export const clearConfig = async (paths) => {
|
|
38
|
+
await rm(paths.configPath, { force: true });
|
|
39
|
+
};
|
|
40
|
+
export const saveConfig = async (paths, config) => {
|
|
41
|
+
await mkdir(paths.storageDir, { recursive: true });
|
|
42
|
+
await writeFile(paths.configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
43
|
+
};
|
|
44
|
+
export const switchStorageDir = async ({ currentConfig, currentPaths, requestedStorageDir, }) => {
|
|
45
|
+
const storageDir = normalizeStorageDir(requestedStorageDir);
|
|
46
|
+
const nextPaths = buildCliPaths(storageDir);
|
|
47
|
+
const targetConfig = await loadConfig(nextPaths.configPath);
|
|
48
|
+
const nextConfig = mergeConfigs(currentConfig, targetConfig);
|
|
49
|
+
await saveConfig(nextPaths, nextConfig);
|
|
50
|
+
await saveGlobalConfig(currentPaths.globalConfigPath, {
|
|
51
|
+
storageDir: storageDir === currentPaths.defaultStorageDir ? undefined : storageDir,
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
paths: nextPaths,
|
|
55
|
+
config: nextConfig,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
const resolveCliPaths = async () => {
|
|
59
|
+
const defaults = getDefaultCliPaths();
|
|
60
|
+
const globalConfig = await loadGlobalConfig(defaults.globalConfigPath);
|
|
61
|
+
const storageDir = normalizeStorageDir(process.env[STORAGE_DIR_ENV_VAR] ?? globalConfig.storageDir ?? defaults.storageDir);
|
|
62
|
+
return buildCliPaths(storageDir);
|
|
63
|
+
};
|
|
64
|
+
const buildCliPaths = (storageDir) => {
|
|
65
|
+
return {
|
|
66
|
+
defaultStorageDir: DEFAULT_STORAGE_DIR,
|
|
67
|
+
storageDir,
|
|
68
|
+
configPath: path.join(storageDir, CONFIG_FILENAME),
|
|
69
|
+
globalConfigPath: path.join(DEFAULT_STORAGE_DIR, GLOBAL_CONFIG_FILENAME),
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const loadConfig = async (configPath) => {
|
|
73
|
+
const parsed = await readJsonFile(configPath, 'failed to read config');
|
|
74
|
+
if (!parsed) {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
baseUrl: typeof parsed.baseUrl === 'string' ? parsed.baseUrl : undefined,
|
|
79
|
+
marketplaceId: typeof parsed.marketplaceId === 'string' ? parsed.marketplaceId : undefined,
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
const loadGlobalConfig = async (globalConfigPath) => {
|
|
83
|
+
const parsed = await readJsonFile(globalConfigPath, 'failed to read global config');
|
|
84
|
+
if (!parsed) {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
storageDir: typeof parsed.storageDir === 'string' ? parsed.storageDir : undefined,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
const saveGlobalConfig = async (globalConfigPath, config) => {
|
|
92
|
+
if (!config.storageDir) {
|
|
93
|
+
await rm(globalConfigPath, { force: true });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
await mkdir(path.dirname(globalConfigPath), { recursive: true });
|
|
97
|
+
await writeFile(globalConfigPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
98
|
+
};
|
|
99
|
+
const readJsonFile = async (filePath, errorMessage) => {
|
|
100
|
+
try {
|
|
101
|
+
const raw = await readFile(filePath, 'utf8');
|
|
102
|
+
return JSON.parse(raw);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error &&
|
|
106
|
+
typeof error === 'object' &&
|
|
107
|
+
'code' in error &&
|
|
108
|
+
error.code === 'ENOENT') {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
throw new CliStorageError('INVALID_CONFIG', errorMessage, { path: filePath });
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const normalizeStorageDir = (value) => {
|
|
115
|
+
const trimmed = value.trim();
|
|
116
|
+
if (!trimmed) {
|
|
117
|
+
throw new CliStorageError('INVALID_INPUT', 'storage dir cannot be empty');
|
|
118
|
+
}
|
|
119
|
+
const expanded = expandHomeDirectory(trimmed);
|
|
120
|
+
return path.resolve(expanded);
|
|
121
|
+
};
|
|
122
|
+
const expandHomeDirectory = (value) => {
|
|
123
|
+
if (value === '~') {
|
|
124
|
+
return homedir();
|
|
125
|
+
}
|
|
126
|
+
if (value.startsWith('~/')) {
|
|
127
|
+
return path.join(homedir(), value.slice(2));
|
|
128
|
+
}
|
|
129
|
+
return value;
|
|
130
|
+
};
|
|
131
|
+
const mergeConfigs = (currentConfig, targetConfig) => {
|
|
132
|
+
return {
|
|
133
|
+
baseUrl: targetConfig.baseUrl ?? currentConfig.baseUrl,
|
|
134
|
+
marketplaceId: targetConfig.marketplaceId ?? currentConfig.marketplaceId,
|
|
135
|
+
};
|
|
136
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CliConfig } from './cli-config';
|
|
2
|
+
import { type HistoryMetricAlias } from './history-response';
|
|
3
|
+
type CliFail = (code: string, message: string, details?: unknown) => never;
|
|
4
|
+
export type CliOptionValues = {
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
marketplace?: string;
|
|
7
|
+
asin?: string[];
|
|
8
|
+
metrics?: string;
|
|
9
|
+
startAt?: string;
|
|
10
|
+
endAt?: string;
|
|
11
|
+
days?: string;
|
|
12
|
+
limit?: string;
|
|
13
|
+
help?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare const requireMarketplaceId: (values: CliOptionValues, config: CliConfig) => string;
|
|
16
|
+
export declare const requireAsins: (commandArgs: string[], values: CliOptionValues, fail: CliFail) => string[];
|
|
17
|
+
export declare const requireSingleAsin: (commandArgs: string[], values: CliOptionValues, fail: CliFail) => string;
|
|
18
|
+
export declare const resolveHistoryMetrics: (values: CliOptionValues, fail: CliFail) => HistoryMetricAlias[];
|
|
19
|
+
export declare const resolveHistoryWindow: (values: CliOptionValues, fail: CliFail) => {
|
|
20
|
+
days: number;
|
|
21
|
+
endAt?: string | undefined;
|
|
22
|
+
startAt?: string | undefined;
|
|
23
|
+
};
|
|
24
|
+
export declare const resolveApiKey: () => string | undefined;
|
|
25
|
+
export declare const resolveBaseUrl: (values: CliOptionValues, config: CliConfig, defaultBaseUrl: string, fail: CliFail) => string;
|
|
26
|
+
export declare const normalizeBaseUrl: (value: string, fail: CliFail) => string;
|
|
27
|
+
export declare const parseIntegerOption: ({ value, optionName, min, max, defaultValue, }: {
|
|
28
|
+
value: string | undefined;
|
|
29
|
+
optionName: string;
|
|
30
|
+
min: number;
|
|
31
|
+
max: number;
|
|
32
|
+
defaultValue: number;
|
|
33
|
+
}, fail: CliFail) => number;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { HISTORY_METRIC_ALIASES } from './history-response.js';
|
|
2
|
+
const ASIN_REGEX = /^[A-Z0-9]{10}$/i;
|
|
3
|
+
const TRAILING_SLASHES_REGEX = /\/+$/;
|
|
4
|
+
const API_SUFFIX_REGEX = /\/api$/i;
|
|
5
|
+
const DEFAULT_MARKETPLACE_ID = 'ATVPDKIKX0DER';
|
|
6
|
+
export const requireMarketplaceId = (values, config) => {
|
|
7
|
+
return values.marketplace ?? config.marketplaceId ?? process.env.RR_MARKETPLACE_ID ?? DEFAULT_MARKETPLACE_ID;
|
|
8
|
+
};
|
|
9
|
+
export const requireAsins = (commandArgs, values, fail) => {
|
|
10
|
+
const candidates = [...commandArgs, ...(values.asin ?? []), ...resolveEnvAsins()]
|
|
11
|
+
.map(value => value.trim())
|
|
12
|
+
.filter(Boolean);
|
|
13
|
+
if (candidates.length === 0) {
|
|
14
|
+
fail('INVALID_INPUT', 'at least one asin is required');
|
|
15
|
+
}
|
|
16
|
+
return Array.from(new Set(candidates.map(value => normalizeAsin(value, fail))));
|
|
17
|
+
};
|
|
18
|
+
export const requireSingleAsin = (commandArgs, values, fail) => {
|
|
19
|
+
const asins = requireAsins(commandArgs, values, fail);
|
|
20
|
+
if (asins.length !== 1) {
|
|
21
|
+
fail('INVALID_INPUT', 'products history requires exactly one asin');
|
|
22
|
+
}
|
|
23
|
+
return asins[0];
|
|
24
|
+
};
|
|
25
|
+
export const resolveHistoryMetrics = (values, fail) => {
|
|
26
|
+
const requested = (values.metrics ?? process.env.RR_HISTORY_METRICS ?? 'bsr,price')
|
|
27
|
+
.split(',')
|
|
28
|
+
.map(value => value.trim().toLowerCase())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
if (requested.length === 0) {
|
|
31
|
+
fail('INVALID_INPUT', 'metrics cannot be empty');
|
|
32
|
+
}
|
|
33
|
+
const invalid = requested.filter(value => !HISTORY_METRIC_ALIASES.includes(value));
|
|
34
|
+
if (invalid.length > 0) {
|
|
35
|
+
fail('INVALID_INPUT', `unsupported history metric: ${invalid[0]}`, {
|
|
36
|
+
supportedMetrics: HISTORY_METRIC_ALIASES,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return Array.from(new Set(requested));
|
|
40
|
+
};
|
|
41
|
+
export const resolveHistoryWindow = (values, fail) => {
|
|
42
|
+
const startAt = values.startAt ? normalizeDateOption('startAt', values.startAt, fail) : undefined;
|
|
43
|
+
const endAt = values.endAt ? normalizeDateOption('endAt', values.endAt, fail) : undefined;
|
|
44
|
+
if (values.days && (startAt || endAt)) {
|
|
45
|
+
fail('INVALID_INPUT', 'use --days or --startAt/--endAt, not both');
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
...(startAt ? { startAt } : {}),
|
|
49
|
+
...(endAt ? { endAt } : {}),
|
|
50
|
+
days: parseIntegerOption({
|
|
51
|
+
value: values.days,
|
|
52
|
+
optionName: 'days',
|
|
53
|
+
min: 30,
|
|
54
|
+
max: 3650,
|
|
55
|
+
defaultValue: 365,
|
|
56
|
+
}, fail),
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
export const resolveApiKey = () => {
|
|
60
|
+
return process.env.RR_LICENSE_KEY;
|
|
61
|
+
};
|
|
62
|
+
export const resolveBaseUrl = (values, config, defaultBaseUrl, fail) => {
|
|
63
|
+
return normalizeBaseUrl(values.baseUrl ?? config.baseUrl ?? process.env.RR_API_URL ?? defaultBaseUrl, fail);
|
|
64
|
+
};
|
|
65
|
+
export const normalizeBaseUrl = (value, fail) => {
|
|
66
|
+
const normalized = value.trim().replace(TRAILING_SLASHES_REGEX, '').replace(API_SUFFIX_REGEX, '');
|
|
67
|
+
if (!normalized) {
|
|
68
|
+
fail('INVALID_INPUT', 'base url cannot be empty');
|
|
69
|
+
}
|
|
70
|
+
return normalized;
|
|
71
|
+
};
|
|
72
|
+
export const parseIntegerOption = ({ value, optionName, min, max, defaultValue, }, fail) => {
|
|
73
|
+
if (!value) {
|
|
74
|
+
return defaultValue;
|
|
75
|
+
}
|
|
76
|
+
const parsed = Number.parseInt(value, 10);
|
|
77
|
+
if (!Number.isFinite(parsed) || parsed < min || parsed > max) {
|
|
78
|
+
fail('INVALID_INPUT', `${optionName} must be an integer between ${min} and ${max}`);
|
|
79
|
+
}
|
|
80
|
+
return parsed;
|
|
81
|
+
};
|
|
82
|
+
const normalizeDateOption = (optionName, value, fail) => {
|
|
83
|
+
const parsed = new Date(value);
|
|
84
|
+
if (!Number.isFinite(parsed.getTime())) {
|
|
85
|
+
fail('INVALID_INPUT', `${optionName} must be a valid date`);
|
|
86
|
+
}
|
|
87
|
+
return parsed.toISOString();
|
|
88
|
+
};
|
|
89
|
+
const normalizeAsin = (value, fail) => {
|
|
90
|
+
const normalized = value.trim().toUpperCase();
|
|
91
|
+
if (!ASIN_REGEX.test(normalized)) {
|
|
92
|
+
fail('INVALID_INPUT', `invalid ASIN: ${value}`);
|
|
93
|
+
}
|
|
94
|
+
return normalized;
|
|
95
|
+
};
|
|
96
|
+
const resolveEnvAsins = () => {
|
|
97
|
+
return [
|
|
98
|
+
...(process.env.RR_ASINS ? process.env.RR_ASINS.split(',') : []),
|
|
99
|
+
...(process.env.RR_ASIN ? [process.env.RR_ASIN] : []),
|
|
100
|
+
];
|
|
101
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const printSuccess: <T>(data: T) => void;
|
|
2
|
+
export declare const fail: (code: string, message: string, details?: unknown) => never;
|
|
3
|
+
export declare const resolveError: (error: unknown) => {
|
|
4
|
+
code: string;
|
|
5
|
+
message: string;
|
|
6
|
+
details: unknown;
|
|
7
|
+
} | {
|
|
8
|
+
code: string;
|
|
9
|
+
message: string;
|
|
10
|
+
details?: undefined;
|
|
11
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { CliStorageError } from './cli-config.js';
|
|
2
|
+
const OUTPUT_PRETTY = true;
|
|
3
|
+
export const printSuccess = (data) => {
|
|
4
|
+
const envelope = {
|
|
5
|
+
ok: true,
|
|
6
|
+
data,
|
|
7
|
+
};
|
|
8
|
+
console.log(JSON.stringify(envelope, null, OUTPUT_PRETTY ? 2 : undefined));
|
|
9
|
+
};
|
|
10
|
+
export const fail = (code, message, details) => {
|
|
11
|
+
const envelope = {
|
|
12
|
+
ok: false,
|
|
13
|
+
error: {
|
|
14
|
+
code,
|
|
15
|
+
message,
|
|
16
|
+
...(details !== undefined ? { details } : {}),
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
console.error(JSON.stringify(envelope, null, OUTPUT_PRETTY ? 2 : undefined));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
};
|
|
22
|
+
export const resolveError = (error) => {
|
|
23
|
+
if (error instanceof CliStorageError) {
|
|
24
|
+
return {
|
|
25
|
+
code: error.code,
|
|
26
|
+
message: error.message,
|
|
27
|
+
details: error.details,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (error && typeof error === 'object' && 'data' in error) {
|
|
31
|
+
const data = error.data;
|
|
32
|
+
if (data?.code) {
|
|
33
|
+
return {
|
|
34
|
+
code: data.code,
|
|
35
|
+
message: error instanceof Error ? error.message : 'Request failed',
|
|
36
|
+
details: data.httpStatus ? { httpStatus: data.httpStatus } : undefined,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (error && typeof error === 'object' && 'message' in error) {
|
|
41
|
+
const message = error.message;
|
|
42
|
+
if (typeof message === 'string') {
|
|
43
|
+
return {
|
|
44
|
+
code: 'REQUEST_FAILED',
|
|
45
|
+
message,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
code: 'UNKNOWN_ERROR',
|
|
51
|
+
message: 'Unknown error',
|
|
52
|
+
};
|
|
53
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export declare const HISTORY_METRIC_ALIASES: readonly ["bsr", "price"];
|
|
2
|
+
export type HistoryMetricAlias = (typeof HISTORY_METRIC_ALIASES)[number];
|
|
3
|
+
type HistoryPointTuple = [string, number | null] | [string, null, 1];
|
|
4
|
+
export type AgentHistorySeries = {
|
|
5
|
+
bsr?: {
|
|
6
|
+
unit: 'rank';
|
|
7
|
+
category: {
|
|
8
|
+
id: number;
|
|
9
|
+
name: string | null;
|
|
10
|
+
} | null;
|
|
11
|
+
points: HistoryPointTuple[];
|
|
12
|
+
};
|
|
13
|
+
price?: {
|
|
14
|
+
unit: 'minorCurrency';
|
|
15
|
+
currencyCode: 'USD';
|
|
16
|
+
valueScale: 100;
|
|
17
|
+
points: HistoryPointTuple[];
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export type AgentHistoryResponse = {
|
|
21
|
+
schemaVersion?: number;
|
|
22
|
+
status?: string;
|
|
23
|
+
syncTriggered: boolean;
|
|
24
|
+
latestImportAt: string | null;
|
|
25
|
+
series?: AgentHistorySeries;
|
|
26
|
+
};
|
|
27
|
+
export declare const buildCliHistoryResponse: ({ asin, marketplaceId, metrics, response, }: {
|
|
28
|
+
asin: string;
|
|
29
|
+
marketplaceId: string;
|
|
30
|
+
metrics: HistoryMetricAlias[];
|
|
31
|
+
response: AgentHistoryResponse;
|
|
32
|
+
}) => {
|
|
33
|
+
schemaVersion: number;
|
|
34
|
+
asin: string;
|
|
35
|
+
marketplaceId: string;
|
|
36
|
+
status: string;
|
|
37
|
+
latestImportAt: string | null;
|
|
38
|
+
syncTriggered: boolean;
|
|
39
|
+
series: AgentHistorySeries;
|
|
40
|
+
};
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const HISTORY_METRIC_ALIASES = ['bsr', 'price'];
|
|
2
|
+
export const buildCliHistoryResponse = ({ asin, marketplaceId, metrics, response, }) => {
|
|
3
|
+
const series = {
|
|
4
|
+
...(metrics.includes('bsr') && response.series?.bsr ? { bsr: response.series.bsr } : {}),
|
|
5
|
+
...(metrics.includes('price') && response.series?.price
|
|
6
|
+
? { price: response.series.price }
|
|
7
|
+
: {}),
|
|
8
|
+
};
|
|
9
|
+
const hasAnyPoints = Object.values(series).some(metricSeries => metricSeries.points.length > 0);
|
|
10
|
+
const status = normalizeHistoryStatus(response.status);
|
|
11
|
+
return {
|
|
12
|
+
schemaVersion: response.schemaVersion ?? 1,
|
|
13
|
+
asin,
|
|
14
|
+
marketplaceId,
|
|
15
|
+
status: status ?? (response.syncTriggered ? 'collecting' : hasAnyPoints ? 'ready' : 'empty'),
|
|
16
|
+
latestImportAt: response.latestImportAt,
|
|
17
|
+
syncTriggered: response.syncTriggered,
|
|
18
|
+
series,
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
const normalizeHistoryStatus = (status) => {
|
|
22
|
+
if (status === 'ready' || status === 'collecting' || status === 'empty') {
|
|
23
|
+
return status;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
2
|
import { parseArgs } from 'node:util';
|
|
6
3
|
import { createRankWranglerClient, DEFAULT_API_BASE_URL } from '@rankwrangler/http-client';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
13
|
-
const DEFAULT_OUTPUT_PRETTY = true;
|
|
4
|
+
import { clearConfig, loadCliContext, loadCliPathsOrDefault, saveConfig, switchStorageDir, } from './cli-config.js';
|
|
5
|
+
import { normalizeBaseUrl, parseIntegerOption, requireAsins, requireMarketplaceId, requireSingleAsin, resolveApiKey, resolveBaseUrl, resolveHistoryMetrics, resolveHistoryWindow, } from './cli-options.js';
|
|
6
|
+
import { fail, printSuccess, resolveError } from './cli-output.js';
|
|
7
|
+
import { buildCliHistoryResponse } from './history-response.js';
|
|
8
|
+
import { printUsage } from './usage.js';
|
|
14
9
|
const SUPPORTED_COMMANDS = new Set([
|
|
15
10
|
'products:get',
|
|
11
|
+
'products:history',
|
|
16
12
|
'license:status',
|
|
17
13
|
'license:validate',
|
|
18
14
|
'config:show',
|
|
@@ -23,56 +19,57 @@ const { positionals, values } = parseArgs({
|
|
|
23
19
|
args: process.argv.slice(2),
|
|
24
20
|
options: {
|
|
25
21
|
help: { type: 'boolean', short: 'h' },
|
|
26
|
-
apiKey: { type: 'string' },
|
|
27
22
|
baseUrl: { type: 'string' },
|
|
28
23
|
marketplace: { type: 'string', short: 'm' },
|
|
29
24
|
asin: { type: 'string', multiple: true },
|
|
25
|
+
metrics: { type: 'string' },
|
|
26
|
+
startAt: { type: 'string' },
|
|
27
|
+
endAt: { type: 'string' },
|
|
28
|
+
days: { type: 'string' },
|
|
29
|
+
limit: { type: 'string' },
|
|
30
30
|
},
|
|
31
31
|
allowPositionals: true,
|
|
32
32
|
});
|
|
33
|
-
const outputPretty = DEFAULT_OUTPUT_PRETTY;
|
|
34
33
|
const main = async () => {
|
|
34
|
+
const optionValues = values;
|
|
35
35
|
if (values.help || positionals.length === 0) {
|
|
36
|
-
printUsage();
|
|
36
|
+
printUsage(await loadCliPathsOrDefault());
|
|
37
37
|
process.exit(values.help ? 0 : 1);
|
|
38
38
|
}
|
|
39
|
-
const command =
|
|
40
|
-
if (!command) {
|
|
41
|
-
fail('UNKNOWN_COMMAND', 'Unknown command', { command: positionals.join(' ') });
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
39
|
+
const command = resolveCommandOrFail(positionals);
|
|
44
40
|
if (!isSupportedCommand(command)) {
|
|
45
|
-
fail('UNKNOWN_COMMAND', 'Unknown command', {
|
|
46
|
-
|
|
41
|
+
fail('UNKNOWN_COMMAND', 'Unknown command', {
|
|
42
|
+
command: `${command.resource} ${command.verb}`,
|
|
43
|
+
});
|
|
47
44
|
}
|
|
48
|
-
const config = await
|
|
45
|
+
const { config, paths } = await loadCliContext();
|
|
49
46
|
if (command.resource === 'config') {
|
|
50
|
-
|
|
51
|
-
printSuccess(response);
|
|
47
|
+
printSuccess(await runConfigCommand(command, config, paths));
|
|
52
48
|
return;
|
|
53
49
|
}
|
|
54
|
-
const apiKey = resolveApiKey(
|
|
50
|
+
const apiKey = resolveApiKey();
|
|
55
51
|
if (!apiKey) {
|
|
56
|
-
fail('MISSING_CONFIG', 'api key is required. set
|
|
57
|
-
return;
|
|
52
|
+
fail('MISSING_CONFIG', 'api key is required. set RR_LICENSE_KEY');
|
|
58
53
|
}
|
|
59
|
-
const baseUrl = resolveBaseUrl(config);
|
|
60
54
|
const client = createRankWranglerClient({
|
|
61
|
-
baseUrl,
|
|
55
|
+
baseUrl: resolveBaseUrl(optionValues, config, DEFAULT_API_BASE_URL, fail),
|
|
62
56
|
apiKey,
|
|
63
57
|
});
|
|
64
|
-
|
|
65
|
-
printSuccess(response);
|
|
58
|
+
printSuccess(await runApiCommand(command, client, config));
|
|
66
59
|
};
|
|
67
60
|
const runApiCommand = async (command, client, config) => {
|
|
68
61
|
if (command.resource === 'products' && command.verb === 'get') {
|
|
69
|
-
const
|
|
70
|
-
const
|
|
62
|
+
const optionValues = values;
|
|
63
|
+
const marketplaceId = requireMarketplaceId(optionValues, config);
|
|
64
|
+
const asins = requireAsins(command.args, optionValues, fail);
|
|
71
65
|
if (asins.length === 1) {
|
|
72
66
|
return client.getProductInfo.mutate({ marketplaceId, asin: asins[0] });
|
|
73
67
|
}
|
|
74
68
|
return client.getProductInfoBatch.mutate({ marketplaceId, asins });
|
|
75
69
|
}
|
|
70
|
+
if (command.resource === 'products' && command.verb === 'history') {
|
|
71
|
+
return runProductHistoryCommand(command, client, config);
|
|
72
|
+
}
|
|
76
73
|
if (command.resource === 'license' && command.verb === 'status') {
|
|
77
74
|
return client.license.status.mutate();
|
|
78
75
|
}
|
|
@@ -82,102 +79,88 @@ const runApiCommand = async (command, client, config) => {
|
|
|
82
79
|
fail('UNKNOWN_COMMAND', 'Unknown command', {
|
|
83
80
|
command: `${command.resource} ${command.verb}`,
|
|
84
81
|
});
|
|
85
|
-
return null;
|
|
86
82
|
};
|
|
87
|
-
const
|
|
83
|
+
const runProductHistoryCommand = async (command, client, config) => {
|
|
84
|
+
const optionValues = values;
|
|
85
|
+
const asin = requireSingleAsin(command.args, optionValues, fail);
|
|
86
|
+
const marketplaceId = requireMarketplaceId(optionValues, config);
|
|
87
|
+
const metrics = resolveHistoryMetrics(optionValues, fail);
|
|
88
|
+
const historyWindow = resolveHistoryWindow(optionValues, fail);
|
|
89
|
+
const limit = parseIntegerOption({
|
|
90
|
+
value: optionValues.limit,
|
|
91
|
+
optionName: 'limit',
|
|
92
|
+
min: 1,
|
|
93
|
+
max: 10000,
|
|
94
|
+
defaultValue: 5000,
|
|
95
|
+
}, fail);
|
|
96
|
+
const response = await client.getProductHistory.mutate({
|
|
97
|
+
marketplaceId,
|
|
98
|
+
asin,
|
|
99
|
+
metrics,
|
|
100
|
+
format: 'agent',
|
|
101
|
+
limit,
|
|
102
|
+
...historyWindow,
|
|
103
|
+
});
|
|
104
|
+
return buildCliHistoryResponse({
|
|
105
|
+
asin,
|
|
106
|
+
marketplaceId,
|
|
107
|
+
metrics,
|
|
108
|
+
response: response,
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
const runConfigCommand = async (command, config, paths) => {
|
|
88
112
|
if (command.verb === 'show') {
|
|
89
|
-
return
|
|
90
|
-
path: CONFIG_PATH,
|
|
91
|
-
config,
|
|
92
|
-
};
|
|
113
|
+
return buildConfigResponse(paths, config);
|
|
93
114
|
}
|
|
94
115
|
if (command.verb === 'clear') {
|
|
95
|
-
await
|
|
116
|
+
await clearConfig(paths);
|
|
96
117
|
return {
|
|
97
|
-
|
|
118
|
+
...buildConfigResponse(paths, {}),
|
|
98
119
|
cleared: true,
|
|
99
120
|
};
|
|
100
121
|
}
|
|
101
|
-
if (command.verb
|
|
102
|
-
|
|
103
|
-
if (!key || valueParts.length === 0) {
|
|
104
|
-
fail('INVALID_INPUT', 'config set requires <key> <value>');
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
const value = valueParts.join(' ').trim();
|
|
108
|
-
if (!value) {
|
|
109
|
-
fail('INVALID_INPUT', 'config set value cannot be empty');
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
const nextConfig = { ...config };
|
|
113
|
-
if (key === 'api-key') {
|
|
114
|
-
nextConfig.apiKey = value;
|
|
115
|
-
}
|
|
116
|
-
else if (key === 'base-url') {
|
|
117
|
-
nextConfig.baseUrl = normalizeBaseUrl(value);
|
|
118
|
-
}
|
|
119
|
-
else if (key === 'marketplace') {
|
|
120
|
-
nextConfig.marketplaceId = value;
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
fail('INVALID_INPUT', 'unsupported config key', {
|
|
124
|
-
key,
|
|
125
|
-
supportedKeys: ['api-key', 'base-url', 'marketplace'],
|
|
126
|
-
});
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
await saveConfig(nextConfig);
|
|
130
|
-
return {
|
|
131
|
-
path: CONFIG_PATH,
|
|
132
|
-
config: nextConfig,
|
|
133
|
-
};
|
|
122
|
+
if (command.verb !== 'set') {
|
|
123
|
+
fail('UNKNOWN_COMMAND', 'Unknown config command', { verb: command.verb });
|
|
134
124
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const requireMarketplaceId = (config) => {
|
|
139
|
-
const marketplaceId = values.marketplace ?? config.marketplaceId ?? process.env.RR_MARKETPLACE_ID ?? DEFAULT_MARKETPLACE_ID;
|
|
140
|
-
return marketplaceId;
|
|
141
|
-
};
|
|
142
|
-
const requireAsins = (commandArgs) => {
|
|
143
|
-
const candidates = collectAsinCandidates(commandArgs);
|
|
144
|
-
if (candidates.length === 0) {
|
|
145
|
-
fail('INVALID_INPUT', 'at least one asin is required');
|
|
125
|
+
const [key, ...valueParts] = command.args;
|
|
126
|
+
if (!key || valueParts.length === 0) {
|
|
127
|
+
fail('INVALID_INPUT', 'config set requires <key> <value>');
|
|
146
128
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const optionAsins = [...(values.asin ?? [])];
|
|
151
|
-
const envAsins = process.env.RR_ASINS
|
|
152
|
-
? process.env.RR_ASINS.split(',').map(value => value.trim())
|
|
153
|
-
: [];
|
|
154
|
-
const envSingleAsin = process.env.RR_ASIN ? [process.env.RR_ASIN] : [];
|
|
155
|
-
return [...commandArgs, ...optionAsins, ...envAsins, ...envSingleAsin]
|
|
156
|
-
.map(value => value.trim())
|
|
157
|
-
.filter(Boolean);
|
|
158
|
-
};
|
|
159
|
-
const normalizeAsin = (value) => {
|
|
160
|
-
const normalized = value.trim().toUpperCase();
|
|
161
|
-
if (!ASIN_REGEX.test(normalized)) {
|
|
162
|
-
fail('INVALID_INPUT', `invalid ASIN: ${value}`);
|
|
129
|
+
const value = valueParts.join(' ').trim();
|
|
130
|
+
if (!value) {
|
|
131
|
+
fail('INVALID_INPUT', 'config set value cannot be empty');
|
|
163
132
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const withoutTrailingSlashes = trimmed.replace(TRAILING_SLASHES_REGEX, '');
|
|
176
|
-
const withoutApiSuffix = withoutTrailingSlashes.replace(API_SUFFIX_REGEX, '');
|
|
177
|
-
if (!withoutApiSuffix) {
|
|
178
|
-
fail('INVALID_INPUT', 'base url cannot be empty');
|
|
133
|
+
if (key === 'storage-dir') {
|
|
134
|
+
const nextState = await switchStorageDir({
|
|
135
|
+
currentConfig: config,
|
|
136
|
+
currentPaths: paths,
|
|
137
|
+
requestedStorageDir: value,
|
|
138
|
+
});
|
|
139
|
+
return buildConfigResponse(nextState.paths, nextState.config);
|
|
140
|
+
}
|
|
141
|
+
const nextConfig = { ...config };
|
|
142
|
+
if (key === 'base-url') {
|
|
143
|
+
nextConfig.baseUrl = normalizeBaseUrl(value, fail);
|
|
179
144
|
}
|
|
180
|
-
|
|
145
|
+
else if (key === 'marketplace') {
|
|
146
|
+
nextConfig.marketplaceId = value;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
fail('INVALID_INPUT', 'unsupported config key', {
|
|
150
|
+
key,
|
|
151
|
+
supportedKeys: ['base-url', 'marketplace', 'storage-dir'],
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
await saveConfig(paths, nextConfig);
|
|
155
|
+
return buildConfigResponse(paths, nextConfig);
|
|
156
|
+
};
|
|
157
|
+
const buildConfigResponse = (paths, config) => {
|
|
158
|
+
return {
|
|
159
|
+
storageDir: paths.storageDir,
|
|
160
|
+
path: paths.configPath,
|
|
161
|
+
globalPath: paths.globalConfigPath,
|
|
162
|
+
config,
|
|
163
|
+
};
|
|
181
164
|
};
|
|
182
165
|
const resolveCommand = (inputPositionals) => {
|
|
183
166
|
const [first, second, ...rest] = inputPositionals;
|
|
@@ -190,121 +173,16 @@ const resolveCommand = (inputPositionals) => {
|
|
|
190
173
|
args: rest,
|
|
191
174
|
};
|
|
192
175
|
};
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
const raw = await readFile(CONFIG_PATH, 'utf8');
|
|
199
|
-
const parsed = JSON.parse(raw);
|
|
200
|
-
return {
|
|
201
|
-
apiKey: typeof parsed.apiKey === 'string' ? parsed.apiKey : undefined,
|
|
202
|
-
baseUrl: typeof parsed.baseUrl === 'string' ? normalizeBaseUrl(parsed.baseUrl) : undefined,
|
|
203
|
-
marketplaceId: typeof parsed.marketplaceId === 'string' ? parsed.marketplaceId : undefined,
|
|
204
|
-
};
|
|
176
|
+
const resolveCommandOrFail = (inputPositionals) => {
|
|
177
|
+
const command = resolveCommand(inputPositionals);
|
|
178
|
+
if (command) {
|
|
179
|
+
return command;
|
|
205
180
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
typeof error === 'object' &&
|
|
209
|
-
'code' in error &&
|
|
210
|
-
error.code === 'ENOENT') {
|
|
211
|
-
return {};
|
|
212
|
-
}
|
|
213
|
-
fail('INVALID_CONFIG', 'failed to read config', { path: CONFIG_PATH });
|
|
214
|
-
return {};
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
const saveConfig = async (config) => {
|
|
218
|
-
await mkdir(CONFIG_DIR, { recursive: true });
|
|
219
|
-
await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
181
|
+
fail('UNKNOWN_COMMAND', 'Unknown command', { command: inputPositionals.join(' ') });
|
|
182
|
+
throw new Error('unreachable');
|
|
220
183
|
};
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
if ('data' in error) {
|
|
224
|
-
const data = error.data;
|
|
225
|
-
if (data?.code) {
|
|
226
|
-
return {
|
|
227
|
-
code: data.code,
|
|
228
|
-
message: error instanceof Error
|
|
229
|
-
? error.message
|
|
230
|
-
: 'Request failed',
|
|
231
|
-
details: data.httpStatus ? { httpStatus: data.httpStatus } : undefined,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
if ('message' in error && typeof error.message === 'string') {
|
|
236
|
-
return {
|
|
237
|
-
code: 'REQUEST_FAILED',
|
|
238
|
-
message: error.message,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return {
|
|
243
|
-
code: 'UNKNOWN_ERROR',
|
|
244
|
-
message: 'Unknown error',
|
|
245
|
-
};
|
|
246
|
-
};
|
|
247
|
-
const printSuccess = (data) => {
|
|
248
|
-
const envelope = {
|
|
249
|
-
ok: true,
|
|
250
|
-
data,
|
|
251
|
-
};
|
|
252
|
-
console.log(JSON.stringify(envelope, null, outputPretty ? 2 : undefined));
|
|
253
|
-
};
|
|
254
|
-
const fail = (code, message, details) => {
|
|
255
|
-
const envelope = {
|
|
256
|
-
ok: false,
|
|
257
|
-
error: {
|
|
258
|
-
code,
|
|
259
|
-
message,
|
|
260
|
-
...(details !== undefined ? { details } : {}),
|
|
261
|
-
},
|
|
262
|
-
};
|
|
263
|
-
console.error(JSON.stringify(envelope, null, outputPretty ? 2 : undefined));
|
|
264
|
-
process.exit(1);
|
|
265
|
-
};
|
|
266
|
-
const printUsage = () => {
|
|
267
|
-
const usage = [
|
|
268
|
-
'NAME',
|
|
269
|
-
' rw, rankwrangler - RankWrangler command line interface',
|
|
270
|
-
'',
|
|
271
|
-
'SYNOPSIS',
|
|
272
|
-
' rw <resource> <verb> [args...] [options]',
|
|
273
|
-
' rankwrangler <resource> <verb> [args...] [options]',
|
|
274
|
-
'',
|
|
275
|
-
'COMMANDS',
|
|
276
|
-
' products get <ASIN...>',
|
|
277
|
-
' license status',
|
|
278
|
-
' license validate',
|
|
279
|
-
' config show',
|
|
280
|
-
' config clear',
|
|
281
|
-
' config set api-key <value>',
|
|
282
|
-
' config set base-url <origin>',
|
|
283
|
-
' config set marketplace <marketplaceId>',
|
|
284
|
-
'',
|
|
285
|
-
'OPTIONS',
|
|
286
|
-
' -h, --help Show this help message',
|
|
287
|
-
' --apiKey <value> Override API key',
|
|
288
|
-
' --baseUrl <origin> Override API origin',
|
|
289
|
-
` -m, --marketplace <id> Override marketplace (default: ${DEFAULT_MARKETPLACE_ID})`,
|
|
290
|
-
' --asin <ASIN> Add ASIN (repeatable)',
|
|
291
|
-
'',
|
|
292
|
-
'FILES',
|
|
293
|
-
` ${CONFIG_PATH} Local CLI config`,
|
|
294
|
-
'',
|
|
295
|
-
'ENVIRONMENT',
|
|
296
|
-
' RR_LICENSE_KEY API key fallback',
|
|
297
|
-
' RR_API_URL API origin fallback',
|
|
298
|
-
' RR_MARKETPLACE_ID Marketplace fallback',
|
|
299
|
-
' RR_ASIN Single ASIN fallback',
|
|
300
|
-
' RR_ASINS Comma-separated ASIN fallback',
|
|
301
|
-
'',
|
|
302
|
-
'EXAMPLES',
|
|
303
|
-
' rw config set api-key rrk_...',
|
|
304
|
-
' rw products get B0DV53VS61',
|
|
305
|
-
' rankwrangler license status',
|
|
306
|
-
];
|
|
307
|
-
console.log(usage.join('\n'));
|
|
184
|
+
const isSupportedCommand = (command) => {
|
|
185
|
+
return SUPPORTED_COMMANDS.has(`${command.resource}:${command.verb}`);
|
|
308
186
|
};
|
|
309
187
|
await main().catch(error => {
|
|
310
188
|
const resolved = resolveError(error);
|
package/dist/usage.d.ts
ADDED
package/dist/usage.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const DEFAULT_MARKETPLACE_ID = 'ATVPDKIKX0DER';
|
|
2
|
+
export const printUsage = (paths) => {
|
|
3
|
+
const usage = [
|
|
4
|
+
'NAME',
|
|
5
|
+
' rw, rankwrangler - RankWrangler command line interface',
|
|
6
|
+
'',
|
|
7
|
+
'SYNOPSIS',
|
|
8
|
+
' rw <resource> <verb> [args...] [options]',
|
|
9
|
+
' rankwrangler <resource> <verb> [args...] [options]',
|
|
10
|
+
'',
|
|
11
|
+
'COMMANDS',
|
|
12
|
+
' products get <ASIN...>',
|
|
13
|
+
' products history <ASIN>',
|
|
14
|
+
' license status',
|
|
15
|
+
' license validate',
|
|
16
|
+
' config show',
|
|
17
|
+
' config clear',
|
|
18
|
+
' config set base-url <origin>',
|
|
19
|
+
' config set marketplace <marketplaceId>',
|
|
20
|
+
' config set storage-dir <path>',
|
|
21
|
+
'',
|
|
22
|
+
'OPTIONS',
|
|
23
|
+
' -h, --help Show this help message',
|
|
24
|
+
' --baseUrl <origin> Override API origin',
|
|
25
|
+
` -m, --marketplace <id> Override marketplace (default: ${DEFAULT_MARKETPLACE_ID})`,
|
|
26
|
+
' --asin <ASIN> Add ASIN (repeatable)',
|
|
27
|
+
' --metrics <list> History metrics: bsr,price (default: bsr,price)',
|
|
28
|
+
' --days <N> History lookback window (30-3650, default: 365)',
|
|
29
|
+
' --startAt <ISO> History range start (ISO date/time)',
|
|
30
|
+
' --endAt <ISO> History range end (ISO date/time)',
|
|
31
|
+
' --limit <N> Max points per metric (1-10000, default: 5000)',
|
|
32
|
+
'',
|
|
33
|
+
'FILES',
|
|
34
|
+
` ${paths.configPath} Active CLI config`,
|
|
35
|
+
` ${paths.globalConfigPath} Global CLI settings`,
|
|
36
|
+
'',
|
|
37
|
+
'ENVIRONMENT',
|
|
38
|
+
' RR_LICENSE_KEY API key source',
|
|
39
|
+
' RR_API_URL API origin fallback',
|
|
40
|
+
' RR_MARKETPLACE_ID Marketplace fallback',
|
|
41
|
+
' RR_STORAGE_DIR Storage directory override',
|
|
42
|
+
' RR_ASIN Single ASIN fallback',
|
|
43
|
+
' RR_ASINS Comma-separated ASIN fallback',
|
|
44
|
+
' RR_HISTORY_METRICS Comma-separated history metrics fallback',
|
|
45
|
+
'',
|
|
46
|
+
'EXAMPLES',
|
|
47
|
+
' RR_LICENSE_KEY=rrk_... rw products get B0DV53VS61',
|
|
48
|
+
' RR_STORAGE_DIR=~/.config/rankwrangler rw config show',
|
|
49
|
+
' rw config set storage-dir ~/.config/rankwrangler',
|
|
50
|
+
' rw products history B0DV53VS61 --metrics bsr,price',
|
|
51
|
+
' rw products history B0DV53VS61 --startAt 2025-01-01 --endAt 2025-12-31',
|
|
52
|
+
' rankwrangler license status',
|
|
53
|
+
];
|
|
54
|
+
console.log(usage.join('\n'));
|
|
55
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rankwrangler/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Official RankWrangler command line interface",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
"README.md"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "tsc -p tsconfig.json"
|
|
17
|
+
"build": "tsc -p tsconfig.json && node scripts/fix-dist-imports.mjs",
|
|
18
|
+
"test:e2e": "bun test test/index.e2e.test.ts"
|
|
18
19
|
},
|
|
19
20
|
"publishConfig": {
|
|
20
21
|
"access": "public"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
|
-
"@rankwrangler/http-client": "^0.
|
|
24
|
+
"@rankwrangler/http-client": "^0.4.0"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@types/node": "^24.3.0",
|