@rankwrangler/cli 0.6.0 → 0.9.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 -66
- package/dist/CHANGELOG.md +58 -1
- package/dist/cli-auth-error.d.ts +5 -0
- package/dist/cli-auth-error.js +10 -0
- package/dist/cli-auth.d.ts +11 -16
- package/dist/cli-auth.js +28 -36
- package/dist/cli-config-command.d.ts +6 -6
- package/dist/cli-keyword-command.d.ts +9 -0
- package/dist/cli-keyword-command.js +69 -0
- package/dist/cli-options.d.ts +8 -6
- package/dist/cli-options.js +34 -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/history-response.d.ts +4 -27
- package/dist/history-response.js +3 -5
- package/dist/index.js +29 -68
- package/dist/usage.js +29 -26
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,75 +1,24 @@
|
|
|
1
1
|
# @rankwrangler/cli
|
|
2
2
|
|
|
3
|
-
Official CLI for RankWrangler.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
3
|
+
Official agent-oriented CLI for RankWrangler.
|
|
6
4
|
|
|
7
5
|
```bash
|
|
8
6
|
npm install -g @rankwrangler/cli
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Usage
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
rw --version
|
|
15
|
-
rw changelog
|
|
16
|
-
rw auth set rrk_...
|
|
17
|
-
printf %s "$RR_LICENSE_KEY" | rw auth set --stdin
|
|
18
|
-
rw config set storage-dir ~/.config/rankwrangler
|
|
19
|
-
rw config get marketplace
|
|
20
|
-
rw config unset marketplace
|
|
21
|
-
rw products get B0DV53VS61
|
|
22
|
-
rw products history B0DV53VS61 --metrics bsr,price --bucket week
|
|
23
|
-
RR_LICENSE_KEY=rrk_... rw license status
|
|
24
|
-
# `rankwrangler` is also supported as an alias
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
`rw auth set [licenseKey]` stores the license key in the platform secure store
|
|
28
|
-
(`macOS Keychain` on macOS). `rw auth status` reports whether the CLI will use an env override,
|
|
29
|
-
stored auth, or no auth. `rw auth set --stdin` reads the key from standard input for agents and
|
|
30
|
-
scripts. `rw auth clear` removes the stored key.
|
|
31
|
-
|
|
32
|
-
`rw config set storage-dir <path>` saves the active storage directory globally. After that, every
|
|
33
|
-
CLI command reads and writes its non-secret config/data from that directory, while preserving
|
|
34
|
-
existing config values when switching. `rw config get <key>`, `rw config unset <key>`, and
|
|
35
|
-
`rw config reset` inspect or remove non-secret config without touching stored auth.
|
|
36
|
-
`RR_LICENSE_KEY`, `RR_STORAGE_DIR`, `RR_API_URL`, and `RR_MARKETPLACE_ID` override saved CLI state
|
|
37
|
-
when set for CI, automation, or agent runtimes.
|
|
38
|
-
`rw products history` ensures product/history data before returning bucketed history (`auto`,
|
|
39
|
-
`day`, `week`, or `month`), not raw point series. `rw --version` prints the installed CLI version.
|
|
40
|
-
`rw changelog` prints the latest bundled release notes shipped with the package.
|
|
41
7
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
8
|
+
rw auth set ak_...
|
|
9
|
+
rw product get B0DV53VS61
|
|
10
|
+
rw product search "retro gardening shirt"
|
|
11
|
+
rw product history B0DV53VS61 --bucket week
|
|
12
|
+
rw keyword get "retro gardening shirt"
|
|
13
|
+
rw keyword search "retro gardening"
|
|
14
|
+
rw keyword history "retro gardening shirt" --rangeDays 90
|
|
47
15
|
```
|
|
48
16
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
bun install
|
|
56
|
-
bun run release:collect-changelog-context
|
|
57
|
-
# draft CHANGELOG.md entry from commit context
|
|
58
|
-
bun run release:check
|
|
59
|
-
bun run cli:build
|
|
60
|
-
bun run release:check-cli-pack
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Then publish:
|
|
64
|
-
|
|
65
|
-
Publish `packages/http-client` first. `packages/cli` now fails fast on `npm publish` until the
|
|
66
|
-
matching `@rankwrangler/http-client` version is already available on npm.
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
cd packages/cli
|
|
70
|
-
node ../../scripts/release/with-npm-token.mjs npm whoami --userconfig ../../.npmrc
|
|
71
|
-
node ../../scripts/release/with-npm-token.mjs npm publish --access public --userconfig ../../.npmrc
|
|
72
|
-
```
|
|
17
|
+
The public command surface is Product `get/search/history`, keyword `get/search/history`, auth,
|
|
18
|
+
config, version, and changelog. Product `get/history` and keyword reads wait for policy-current
|
|
19
|
+
data and expose no refresh control; Product search retains its separate search contract. Search
|
|
20
|
+
data contains `keyword`, `searchedAt`, and compact results with `organicSearchPlacement`. Output is
|
|
21
|
+
a JSON `{ ok, data }` envelope; failures use `{ ok: false, error }` on stderr.
|
|
22
|
+
The CLI does not expose Catalog, Operation, provider status, or polling commands.
|
|
73
23
|
|
|
74
|
-
|
|
75
|
-
Keychain item `rankwrangler-npm-token` for account `$USER`.
|
|
24
|
+
See the [CLI reference](../../docs/reference/cli.md) for options and release workflow.
|
package/dist/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## v0.9.0 - 2026-08-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add short-lived typed Provider-attempt telemetry for Keepa and SP-API operations, including
|
|
10
|
+
outcome, latency, and error context for operator diagnostics.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Breaking: replace the `product.getMany` available/unavailable result union with one fixed shape
|
|
15
|
+
containing identity, nullable title, resolved thumbnail, and `isUnavailable`. Unavailable Amazon
|
|
16
|
+
listings retain their last-known title and thumbnail data.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Advertise Claude-compatible MCP Product and keyword schemas so fresh chats can discover and call
|
|
21
|
+
`getMany` directly, with clear unavailable-listing semantics in the Product tool description.
|
|
22
|
+
- Stop unavailable Products from triggering automatic Keepa synchronization when their dashboard
|
|
23
|
+
drawer opens.
|
|
24
|
+
|
|
25
|
+
## v0.8.0 - 2026-08-09
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Add synchronous basic Product lookup for up to 200 marketplace/ASIN pairs through the public
|
|
30
|
+
API and hosted MCP, with smart single- or multi-ASIN routing in `rw product get`.
|
|
31
|
+
- Add complete Product Search, source provenance, and nullable merch-listing classification to the
|
|
32
|
+
public Product intelligence surface.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Resolve public Product reads to policy-current, source-attributed results while batching cold
|
|
37
|
+
SP-API catalog work by marketplace and persisting every requested Product identity.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- Preserve unavailable listing data, stop repeatedly polling unavailable listings, and keep those
|
|
42
|
+
Products accessible from the dashboard drawer.
|
|
43
|
+
- Expose the hosted MCP ingress correctly in production.
|
|
44
|
+
|
|
45
|
+
## v0.7.0 - 2026-08-06
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- Add typed Product and keyword `get`, `search`, and `history` contracts for agents and integrations.
|
|
50
|
+
- Add Merchbase OAuth bearer authentication and centralized RankWrangler access metering across
|
|
51
|
+
public clients.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- Breaking: replace the previous public client surface with the grouped `product.get`,
|
|
56
|
+
`product.search`, `product.history`, `keyword.get`, `keyword.search`, and `keyword.history`
|
|
57
|
+
procedures.
|
|
58
|
+
- Return final source-attributed data and freshness through the public API while keeping Catalog,
|
|
59
|
+
Operation, polling, and provider orchestration internal.
|
|
60
|
+
|
|
5
61
|
## v0.6.0 - 2026-06-15
|
|
6
62
|
|
|
7
63
|
### Added
|
|
@@ -220,4 +276,5 @@ All notable changes to this project will be documented in this file.
|
|
|
220
276
|
|
|
221
277
|
- Rename and standardize the typed npm client package to `@merchbase/rankwrangler-http-client`.
|
|
222
278
|
- Set initial HTTP client package release to `0.1.0` in `packages/http-client/package.json`.
|
|
223
|
-
- Document npm publish and versioning policy for the typed HTTP client
|
|
279
|
+
- Document npm publish and versioning policy for the typed HTTP client; the current owner is
|
|
280
|
+
`docs/reference/http-client.md`.
|
package/dist/cli-auth.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export declare const
|
|
2
|
-
export declare const AUTH_SERVICE_NAME = "
|
|
3
|
-
export declare const AUTH_ACCOUNT_NAME = "
|
|
1
|
+
export declare const API_KEY_ENV_VAR = "MERCHBASE_API_KEY";
|
|
2
|
+
export declare const AUTH_SERVICE_NAME = "co.merchbase.cli";
|
|
3
|
+
export declare const AUTH_ACCOUNT_NAME = "api-key";
|
|
4
4
|
type CliFail = (code: string, message: string, details?: unknown) => never;
|
|
5
5
|
type AuthCommand = {
|
|
6
6
|
verb: string;
|
|
@@ -9,11 +9,6 @@ type AuthCommand = {
|
|
|
9
9
|
type AuthCommandOptions = {
|
|
10
10
|
stdin?: boolean;
|
|
11
11
|
};
|
|
12
|
-
export declare class CliAuthError extends Error {
|
|
13
|
-
readonly code: string;
|
|
14
|
-
readonly details?: unknown;
|
|
15
|
-
constructor(code: string, message: string, details?: unknown);
|
|
16
|
-
}
|
|
17
12
|
export declare const resolveApiKey: () => Promise<string | undefined>;
|
|
18
13
|
export declare const runAuthCommand: (command: AuthCommand, fail: CliFail, options?: AuthCommandOptions) => Promise<{
|
|
19
14
|
source: string;
|
|
@@ -23,7 +18,7 @@ export declare const runAuthCommand: (command: AuthCommand, fail: CliFail, optio
|
|
|
23
18
|
serviceName: string;
|
|
24
19
|
accountName: string;
|
|
25
20
|
available: boolean;
|
|
26
|
-
|
|
21
|
+
hasStoredApiKey: boolean;
|
|
27
22
|
error?: undefined;
|
|
28
23
|
};
|
|
29
24
|
} | {
|
|
@@ -34,7 +29,7 @@ export declare const runAuthCommand: (command: AuthCommand, fail: CliFail, optio
|
|
|
34
29
|
serviceName: string;
|
|
35
30
|
accountName: string;
|
|
36
31
|
available: boolean;
|
|
37
|
-
|
|
32
|
+
hasStoredApiKey: null;
|
|
38
33
|
error: string;
|
|
39
34
|
};
|
|
40
35
|
} | {
|
|
@@ -46,7 +41,7 @@ export declare const runAuthCommand: (command: AuthCommand, fail: CliFail, optio
|
|
|
46
41
|
serviceName: string;
|
|
47
42
|
accountName: string;
|
|
48
43
|
available: boolean;
|
|
49
|
-
|
|
44
|
+
hasStoredApiKey: boolean;
|
|
50
45
|
error?: undefined;
|
|
51
46
|
};
|
|
52
47
|
} | {
|
|
@@ -58,7 +53,7 @@ export declare const runAuthCommand: (command: AuthCommand, fail: CliFail, optio
|
|
|
58
53
|
serviceName: string;
|
|
59
54
|
accountName: string;
|
|
60
55
|
available: boolean;
|
|
61
|
-
|
|
56
|
+
hasStoredApiKey: null;
|
|
62
57
|
error: string;
|
|
63
58
|
};
|
|
64
59
|
} | {
|
|
@@ -70,7 +65,7 @@ export declare const runAuthCommand: (command: AuthCommand, fail: CliFail, optio
|
|
|
70
65
|
serviceName: string;
|
|
71
66
|
accountName: string;
|
|
72
67
|
available: boolean;
|
|
73
|
-
|
|
68
|
+
hasStoredApiKey: boolean;
|
|
74
69
|
error?: undefined;
|
|
75
70
|
};
|
|
76
71
|
} | {
|
|
@@ -82,7 +77,7 @@ export declare const runAuthCommand: (command: AuthCommand, fail: CliFail, optio
|
|
|
82
77
|
serviceName: string;
|
|
83
78
|
accountName: string;
|
|
84
79
|
available: boolean;
|
|
85
|
-
|
|
80
|
+
hasStoredApiKey: null;
|
|
86
81
|
error: string;
|
|
87
82
|
};
|
|
88
83
|
}>;
|
|
@@ -94,7 +89,7 @@ export declare const buildAuthStatus: () => Promise<{
|
|
|
94
89
|
serviceName: string;
|
|
95
90
|
accountName: string;
|
|
96
91
|
available: boolean;
|
|
97
|
-
|
|
92
|
+
hasStoredApiKey: boolean;
|
|
98
93
|
error?: undefined;
|
|
99
94
|
};
|
|
100
95
|
} | {
|
|
@@ -105,7 +100,7 @@ export declare const buildAuthStatus: () => Promise<{
|
|
|
105
100
|
serviceName: string;
|
|
106
101
|
accountName: string;
|
|
107
102
|
available: boolean;
|
|
108
|
-
|
|
103
|
+
hasStoredApiKey: null;
|
|
109
104
|
error: string;
|
|
110
105
|
};
|
|
111
106
|
}>;
|
package/dist/cli-auth.js
CHANGED
|
@@ -2,24 +2,16 @@ import { spawnSync } from 'node:child_process';
|
|
|
2
2
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import readline from 'node:readline';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export const
|
|
5
|
+
import { MERCHBASE_API_KEY_ENV, MERCHBASE_API_KEY_KEYCHAIN_ACCOUNT, MERCHBASE_API_KEY_KEYCHAIN_SERVICE, } from '@merchbaseco/access';
|
|
6
|
+
import { CliAuthError } from './cli-auth-error.js';
|
|
7
|
+
export const API_KEY_ENV_VAR = MERCHBASE_API_KEY_ENV;
|
|
8
|
+
export const AUTH_SERVICE_NAME = MERCHBASE_API_KEY_KEYCHAIN_SERVICE;
|
|
9
|
+
export const AUTH_ACCOUNT_NAME = MERCHBASE_API_KEY_KEYCHAIN_ACCOUNT;
|
|
8
10
|
const INTERNAL_SECRET_STORE_DIR_ENV_VAR = 'RW_INTERNAL_CLI_SECRET_STORE_DIR';
|
|
9
|
-
export class CliAuthError extends Error {
|
|
10
|
-
code;
|
|
11
|
-
details;
|
|
12
|
-
constructor(code, message, details) {
|
|
13
|
-
super(message);
|
|
14
|
-
this.name = 'CliAuthError';
|
|
15
|
-
this.code = code;
|
|
16
|
-
this.details = details;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
11
|
export const resolveApiKey = async () => {
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
return
|
|
12
|
+
const envApiKey = resolveEnvApiKey();
|
|
13
|
+
if (envApiKey) {
|
|
14
|
+
return envApiKey;
|
|
23
15
|
}
|
|
24
16
|
const secureStore = await createSecureStore();
|
|
25
17
|
return (await secureStore.get()) ?? undefined;
|
|
@@ -48,32 +40,32 @@ export const runAuthCommand = async (command, fail, options = {}) => {
|
|
|
48
40
|
fail('UNKNOWN_COMMAND', 'Unknown auth command', { verb: command.verb });
|
|
49
41
|
};
|
|
50
42
|
export const buildAuthStatus = async () => {
|
|
51
|
-
const
|
|
43
|
+
const envApiKey = resolveEnvApiKey();
|
|
52
44
|
try {
|
|
53
45
|
const secureStore = await createSecureStore();
|
|
54
|
-
const
|
|
46
|
+
const storedApiKey = await secureStore.get();
|
|
55
47
|
return {
|
|
56
|
-
source:
|
|
57
|
-
envOverride: Boolean(
|
|
48
|
+
source: envApiKey ? 'env' : storedApiKey ? 'secure-store' : 'none',
|
|
49
|
+
envOverride: Boolean(envApiKey),
|
|
58
50
|
secureStore: {
|
|
59
51
|
backend: secureStore.backend,
|
|
60
52
|
serviceName: secureStore.serviceName,
|
|
61
53
|
accountName: secureStore.accountName,
|
|
62
54
|
available: true,
|
|
63
|
-
|
|
55
|
+
hasStoredApiKey: Boolean(storedApiKey),
|
|
64
56
|
},
|
|
65
57
|
};
|
|
66
58
|
}
|
|
67
59
|
catch (error) {
|
|
68
60
|
return {
|
|
69
|
-
source:
|
|
70
|
-
envOverride: Boolean(
|
|
61
|
+
source: envApiKey ? 'env' : 'none',
|
|
62
|
+
envOverride: Boolean(envApiKey),
|
|
71
63
|
secureStore: {
|
|
72
64
|
backend: getSecureStoreBackendLabel(),
|
|
73
65
|
serviceName: AUTH_SERVICE_NAME,
|
|
74
66
|
accountName: AUTH_ACCOUNT_NAME,
|
|
75
67
|
available: false,
|
|
76
|
-
|
|
68
|
+
hasStoredApiKey: null,
|
|
77
69
|
error: error instanceof Error ? error.message : 'Unknown secure-store error',
|
|
78
70
|
},
|
|
79
71
|
};
|
|
@@ -89,20 +81,20 @@ const resolveAuthSetValue = async (args, fail, options) => {
|
|
|
89
81
|
if (stdinValue) {
|
|
90
82
|
return stdinValue;
|
|
91
83
|
}
|
|
92
|
-
fail('INVALID_INPUT', 'auth set --stdin received an empty
|
|
84
|
+
fail('INVALID_INPUT', 'auth set --stdin received an empty API key');
|
|
93
85
|
}
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
return
|
|
86
|
+
const envApiKey = resolveEnvApiKey();
|
|
87
|
+
if (envApiKey) {
|
|
88
|
+
return envApiKey;
|
|
97
89
|
}
|
|
98
90
|
if (process.stdin.isTTY && process.stderr.isTTY) {
|
|
99
|
-
const promptValue = await readSecretFromPrompt('
|
|
91
|
+
const promptValue = await readSecretFromPrompt('Merchbase API key: ');
|
|
100
92
|
if (promptValue) {
|
|
101
93
|
return promptValue;
|
|
102
94
|
}
|
|
103
|
-
fail('INVALID_INPUT', 'auth set received an empty
|
|
95
|
+
fail('INVALID_INPUT', 'auth set received an empty API key');
|
|
104
96
|
}
|
|
105
|
-
fail('INVALID_INPUT', `auth set requires <
|
|
97
|
+
fail('INVALID_INPUT', `auth set requires <apiKey>, --stdin, or ${API_KEY_ENV_VAR}`);
|
|
106
98
|
};
|
|
107
99
|
const readSecretFromStdin = async () => {
|
|
108
100
|
const chunks = [];
|
|
@@ -139,8 +131,8 @@ const setTerminalEcho = (enabled) => {
|
|
|
139
131
|
stdio: ['inherit', 'ignore', 'ignore'],
|
|
140
132
|
});
|
|
141
133
|
};
|
|
142
|
-
const
|
|
143
|
-
const value = process.env[
|
|
134
|
+
const resolveEnvApiKey = () => {
|
|
135
|
+
const value = process.env[API_KEY_ENV_VAR]?.trim();
|
|
144
136
|
return value ? value : undefined;
|
|
145
137
|
};
|
|
146
138
|
const createSecureStore = async () => {
|
|
@@ -178,7 +170,7 @@ const getSecureStoreBackendLabel = () => {
|
|
|
178
170
|
}
|
|
179
171
|
};
|
|
180
172
|
const createFileSecretStore = (secretStoreDir) => {
|
|
181
|
-
const secretPath = path.join(secretStoreDir, '
|
|
173
|
+
const secretPath = path.join(secretStoreDir, 'api-key.json');
|
|
182
174
|
return {
|
|
183
175
|
backend: 'test-file-store',
|
|
184
176
|
serviceName: AUTH_SERVICE_NAME,
|
|
@@ -187,7 +179,7 @@ const createFileSecretStore = (secretStoreDir) => {
|
|
|
187
179
|
try {
|
|
188
180
|
const raw = await readFile(secretPath, 'utf8');
|
|
189
181
|
const parsed = JSON.parse(raw);
|
|
190
|
-
return typeof parsed.
|
|
182
|
+
return typeof parsed.apiKey === 'string' ? parsed.apiKey : null;
|
|
191
183
|
}
|
|
192
184
|
catch (error) {
|
|
193
185
|
if (error &&
|
|
@@ -201,7 +193,7 @@ const createFileSecretStore = (secretStoreDir) => {
|
|
|
201
193
|
},
|
|
202
194
|
set: async (value) => {
|
|
203
195
|
await mkdir(secretStoreDir, { recursive: true });
|
|
204
|
-
await writeFile(secretPath, `${JSON.stringify({
|
|
196
|
+
await writeFile(secretPath, `${JSON.stringify({ apiKey: value }, null, 2)}\n`, 'utf8');
|
|
205
197
|
},
|
|
206
198
|
clear: async () => {
|
|
207
199
|
try {
|
|
@@ -17,7 +17,7 @@ export declare const runConfigCommand: (command: ConfigCommand, config: CliConfi
|
|
|
17
17
|
serviceName: string;
|
|
18
18
|
accountName: string;
|
|
19
19
|
available: boolean;
|
|
20
|
-
|
|
20
|
+
hasStoredApiKey: boolean;
|
|
21
21
|
error?: undefined;
|
|
22
22
|
};
|
|
23
23
|
} | {
|
|
@@ -28,7 +28,7 @@ export declare const runConfigCommand: (command: ConfigCommand, config: CliConfi
|
|
|
28
28
|
serviceName: string;
|
|
29
29
|
accountName: string;
|
|
30
30
|
available: boolean;
|
|
31
|
-
|
|
31
|
+
hasStoredApiKey: null;
|
|
32
32
|
error: string;
|
|
33
33
|
};
|
|
34
34
|
};
|
|
@@ -49,7 +49,7 @@ export declare const runConfigCommand: (command: ConfigCommand, config: CliConfi
|
|
|
49
49
|
serviceName: string;
|
|
50
50
|
accountName: string;
|
|
51
51
|
available: boolean;
|
|
52
|
-
|
|
52
|
+
hasStoredApiKey: boolean;
|
|
53
53
|
error?: undefined;
|
|
54
54
|
};
|
|
55
55
|
} | {
|
|
@@ -60,7 +60,7 @@ export declare const runConfigCommand: (command: ConfigCommand, config: CliConfi
|
|
|
60
60
|
serviceName: string;
|
|
61
61
|
accountName: string;
|
|
62
62
|
available: boolean;
|
|
63
|
-
|
|
63
|
+
hasStoredApiKey: null;
|
|
64
64
|
error: string;
|
|
65
65
|
};
|
|
66
66
|
};
|
|
@@ -80,7 +80,7 @@ export declare const runConfigCommand: (command: ConfigCommand, config: CliConfi
|
|
|
80
80
|
serviceName: string;
|
|
81
81
|
accountName: string;
|
|
82
82
|
available: boolean;
|
|
83
|
-
|
|
83
|
+
hasStoredApiKey: boolean;
|
|
84
84
|
error?: undefined;
|
|
85
85
|
};
|
|
86
86
|
} | {
|
|
@@ -91,7 +91,7 @@ export declare const runConfigCommand: (command: ConfigCommand, config: CliConfi
|
|
|
91
91
|
serviceName: string;
|
|
92
92
|
accountName: string;
|
|
93
93
|
available: boolean;
|
|
94
|
-
|
|
94
|
+
hasStoredApiKey: null;
|
|
95
95
|
error: string;
|
|
96
96
|
};
|
|
97
97
|
};
|
|
@@ -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,24 @@
|
|
|
1
1
|
import type { CliConfig } from './cli-config';
|
|
2
|
-
import { type HistoryBucket, 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
13
|
bucket?: string;
|
|
14
|
+
cursor?: string;
|
|
15
|
+
refresh?: boolean;
|
|
14
16
|
help?: boolean;
|
|
15
17
|
version?: boolean;
|
|
16
|
-
}
|
|
18
|
+
}
|
|
17
19
|
export declare const requireMarketplaceId: (values: CliOptionValues, config: CliConfig) => string;
|
|
18
|
-
export declare const
|
|
19
|
-
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[];
|
|
20
22
|
export declare const resolveHistoryMetrics: (values: CliOptionValues, fail: CliFail) => HistoryMetricAlias[];
|
|
21
23
|
export declare const resolveHistoryBucket: (values: CliOptionValues, fail: CliFail) => HistoryBucket;
|
|
22
24
|
export declare const resolveHistoryWindow: (values: CliOptionValues, fail: CliFail) => {
|
package/dist/cli-options.js
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
|
-
import { HISTORY_BUCKETS, 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');
|
|
@@ -48,7 +51,9 @@ export const resolveHistoryBucket = (values, fail) => {
|
|
|
48
51
|
return requested;
|
|
49
52
|
};
|
|
50
53
|
export const resolveHistoryWindow = (values, fail) => {
|
|
51
|
-
const startAt = values.startAt
|
|
54
|
+
const startAt = values.startAt
|
|
55
|
+
? normalizeDateOption('startAt', values.startAt, fail)
|
|
56
|
+
: undefined;
|
|
52
57
|
const endAt = values.endAt ? normalizeDateOption('endAt', values.endAt, fail) : undefined;
|
|
53
58
|
if (values.days && (startAt || endAt)) {
|
|
54
59
|
fail('INVALID_INPUT', 'use --days or --startAt/--endAt, not both');
|
|
@@ -69,7 +74,10 @@ export const resolveBaseUrl = (values, config, defaultBaseUrl, fail) => {
|
|
|
69
74
|
return normalizeBaseUrl(values.baseUrl ?? config.baseUrl ?? process.env.RR_API_URL ?? defaultBaseUrl, fail);
|
|
70
75
|
};
|
|
71
76
|
export const normalizeBaseUrl = (value, fail) => {
|
|
72
|
-
const normalized = value
|
|
77
|
+
const normalized = value
|
|
78
|
+
.trim()
|
|
79
|
+
.replace(TRAILING_SLASHES_REGEX, '')
|
|
80
|
+
.replace(API_SUFFIX_REGEX, '');
|
|
73
81
|
if (!normalized) {
|
|
74
82
|
fail('INVALID_INPUT', 'base url cannot be empty');
|
|
75
83
|
}
|
|
@@ -99,9 +107,12 @@ const normalizeAsin = (value, fail) => {
|
|
|
99
107
|
}
|
|
100
108
|
return normalized;
|
|
101
109
|
};
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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;
|
|
107
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];
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
export declare const HISTORY_METRIC_ALIASES: readonly ["bsr", "price"];
|
|
2
|
-
export declare const HISTORY_BUCKETS: readonly ["auto", "day", "week", "month"];
|
|
3
2
|
export type HistoryMetricAlias = (typeof HISTORY_METRIC_ALIASES)[number];
|
|
4
|
-
|
|
5
|
-
type HistoryBucketTuple = [string, number | null];
|
|
6
|
-
type HistoryBucketSummary = {
|
|
7
|
-
first: number | null;
|
|
8
|
-
latest: number | null;
|
|
9
|
-
min: number | null;
|
|
10
|
-
max: number | null;
|
|
11
|
-
count: number;
|
|
12
|
-
firstBucketAt: string | null;
|
|
13
|
-
latestBucketAt: string | null;
|
|
14
|
-
};
|
|
3
|
+
type HistoryPointTuple = [string, number | null] | [string, null, 1];
|
|
15
4
|
export type AgentHistorySeries = {
|
|
16
5
|
bsr?: {
|
|
17
6
|
unit: 'rank';
|
|
@@ -19,15 +8,13 @@ export type AgentHistorySeries = {
|
|
|
19
8
|
id: number;
|
|
20
9
|
name: string | null;
|
|
21
10
|
} | null;
|
|
22
|
-
|
|
23
|
-
summary: HistoryBucketSummary;
|
|
11
|
+
points: HistoryPointTuple[];
|
|
24
12
|
};
|
|
25
13
|
price?: {
|
|
26
14
|
unit: 'minorCurrency';
|
|
27
15
|
currencyCode: 'USD';
|
|
28
16
|
valueScale: 100;
|
|
29
|
-
|
|
30
|
-
summary: HistoryBucketSummary;
|
|
17
|
+
points: HistoryPointTuple[];
|
|
31
18
|
};
|
|
32
19
|
};
|
|
33
20
|
export type AgentHistoryResponse = {
|
|
@@ -35,11 +22,6 @@ export type AgentHistoryResponse = {
|
|
|
35
22
|
status?: string;
|
|
36
23
|
syncTriggered: boolean;
|
|
37
24
|
latestImportAt: string | null;
|
|
38
|
-
range?: {
|
|
39
|
-
startAt: string;
|
|
40
|
-
endAt: string;
|
|
41
|
-
bucket: Exclude<HistoryBucket, 'auto'>;
|
|
42
|
-
};
|
|
43
25
|
series?: AgentHistorySeries;
|
|
44
26
|
};
|
|
45
27
|
export declare const buildCliHistoryResponse: ({ asin, marketplaceId, metrics, response, }: {
|
|
@@ -48,17 +30,12 @@ export declare const buildCliHistoryResponse: ({ asin, marketplaceId, metrics, r
|
|
|
48
30
|
metrics: HistoryMetricAlias[];
|
|
49
31
|
response: AgentHistoryResponse;
|
|
50
32
|
}) => {
|
|
51
|
-
series: AgentHistorySeries;
|
|
52
|
-
range?: {
|
|
53
|
-
startAt: string;
|
|
54
|
-
endAt: string;
|
|
55
|
-
bucket: Exclude<HistoryBucket, "auto">;
|
|
56
|
-
} | undefined;
|
|
57
33
|
schemaVersion: number;
|
|
58
34
|
asin: string;
|
|
59
35
|
marketplaceId: string;
|
|
60
36
|
status: string;
|
|
61
37
|
latestImportAt: string | null;
|
|
62
38
|
syncTriggered: boolean;
|
|
39
|
+
series: AgentHistorySeries;
|
|
63
40
|
};
|
|
64
41
|
export {};
|
package/dist/history-response.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export const HISTORY_METRIC_ALIASES = ['bsr', 'price'];
|
|
2
|
-
export const HISTORY_BUCKETS = ['auto', 'day', 'week', 'month'];
|
|
3
2
|
export const buildCliHistoryResponse = ({ asin, marketplaceId, metrics, response, }) => {
|
|
4
3
|
const series = {
|
|
5
4
|
...(metrics.includes('bsr') && response.series?.bsr ? { bsr: response.series.bsr } : {}),
|
|
@@ -7,16 +6,15 @@ export const buildCliHistoryResponse = ({ asin, marketplaceId, metrics, response
|
|
|
7
6
|
? { price: response.series.price }
|
|
8
7
|
: {}),
|
|
9
8
|
};
|
|
10
|
-
const
|
|
9
|
+
const hasAnyPoints = Object.values(series).some(metricSeries => metricSeries.points.length > 0);
|
|
11
10
|
const status = normalizeHistoryStatus(response.status);
|
|
12
11
|
return {
|
|
13
|
-
schemaVersion: response.schemaVersion ??
|
|
12
|
+
schemaVersion: response.schemaVersion ?? 1,
|
|
14
13
|
asin,
|
|
15
14
|
marketplaceId,
|
|
16
|
-
status: status ?? (response.syncTriggered ? 'collecting' :
|
|
15
|
+
status: status ?? (response.syncTriggered ? 'collecting' : hasAnyPoints ? 'ready' : 'empty'),
|
|
17
16
|
latestImportAt: response.latestImportAt,
|
|
18
17
|
syncTriggered: response.syncTriggered,
|
|
19
|
-
...(response.range ? { range: response.range } : {}),
|
|
20
18
|
series,
|
|
21
19
|
};
|
|
22
20
|
};
|
package/dist/index.js
CHANGED
|
@@ -2,18 +2,21 @@
|
|
|
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',
|
|
@@ -30,13 +33,15 @@ const { positionals, values } = parseArgs({
|
|
|
30
33
|
version: { type: 'boolean' },
|
|
31
34
|
baseUrl: { type: 'string' },
|
|
32
35
|
marketplace: { type: 'string', short: 'm' },
|
|
33
|
-
asin: { type: 'string', multiple: true },
|
|
34
36
|
metrics: { type: 'string' },
|
|
35
37
|
startAt: { type: 'string' },
|
|
36
38
|
endAt: { type: 'string' },
|
|
37
39
|
days: { type: 'string' },
|
|
40
|
+
rangeDays: { type: 'string' },
|
|
38
41
|
limit: { type: 'string' },
|
|
39
42
|
bucket: { type: 'string' },
|
|
43
|
+
cursor: { type: 'string' },
|
|
44
|
+
refresh: { type: 'boolean' },
|
|
40
45
|
stdin: { type: 'boolean' },
|
|
41
46
|
},
|
|
42
47
|
allowPositionals: true,
|
|
@@ -52,11 +57,9 @@ const main = async () => {
|
|
|
52
57
|
process.exit(values.help ? 0 : 1);
|
|
53
58
|
}
|
|
54
59
|
const metaCommand = resolveMetaCommand(positionals);
|
|
55
|
-
if (metaCommand) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
+
if (metaCommand?.name === 'changelog') {
|
|
61
|
+
printBundledChangelog();
|
|
62
|
+
return;
|
|
60
63
|
}
|
|
61
64
|
const command = resolveCommandOrFail(positionals);
|
|
62
65
|
if (!isSupportedCommand(command)) {
|
|
@@ -70,72 +73,32 @@ const main = async () => {
|
|
|
70
73
|
return;
|
|
71
74
|
}
|
|
72
75
|
if (command.resource === 'auth') {
|
|
73
|
-
printSuccess(await runAuthCommand(command, fail, {
|
|
76
|
+
printSuccess(await runAuthCommand(command, fail, {
|
|
77
|
+
stdin: Boolean(values.stdin),
|
|
78
|
+
}));
|
|
74
79
|
return;
|
|
75
80
|
}
|
|
76
81
|
const apiKey = await resolveApiKey();
|
|
77
82
|
if (!apiKey) {
|
|
78
|
-
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');
|
|
79
84
|
}
|
|
80
85
|
const client = createRankWranglerClient({
|
|
81
86
|
baseUrl: resolveBaseUrl(optionValues, config, DEFAULT_API_BASE_URL, fail),
|
|
82
87
|
apiKey,
|
|
83
88
|
});
|
|
84
|
-
printSuccess(await runApiCommand(command, client, config));
|
|
89
|
+
printSuccess(await runApiCommand(command, client, config, optionValues));
|
|
85
90
|
};
|
|
86
|
-
const runApiCommand = async (command, client, config) => {
|
|
87
|
-
if (command.resource === '
|
|
88
|
-
|
|
89
|
-
const marketplaceId = requireMarketplaceId(optionValues, config);
|
|
90
|
-
const asins = requireAsins(command.args, optionValues, fail);
|
|
91
|
-
if (asins.length === 1) {
|
|
92
|
-
return client.getProductInfo.mutate({ marketplaceId, asin: asins[0] });
|
|
93
|
-
}
|
|
94
|
-
return client.getProductInfoBatch.mutate({ marketplaceId, asins });
|
|
95
|
-
}
|
|
96
|
-
if (command.resource === 'products' && command.verb === 'history') {
|
|
97
|
-
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);
|
|
98
94
|
}
|
|
99
|
-
if (command.resource === '
|
|
100
|
-
return client
|
|
101
|
-
}
|
|
102
|
-
if (command.resource === 'license' && command.verb === 'validate') {
|
|
103
|
-
return client.license.validate.mutate();
|
|
95
|
+
if (command.resource === 'keyword') {
|
|
96
|
+
return await runKeywordCommand(command, client, options, fail);
|
|
104
97
|
}
|
|
105
98
|
fail('UNKNOWN_COMMAND', 'Unknown command', {
|
|
106
99
|
command: `${command.resource} ${command.verb}`,
|
|
107
100
|
});
|
|
108
101
|
};
|
|
109
|
-
const runProductHistoryCommand = async (command, client, config) => {
|
|
110
|
-
const optionValues = values;
|
|
111
|
-
const asin = requireSingleAsin(command.args, optionValues, fail);
|
|
112
|
-
const marketplaceId = requireMarketplaceId(optionValues, config);
|
|
113
|
-
const metrics = resolveHistoryMetrics(optionValues, fail);
|
|
114
|
-
const bucket = resolveHistoryBucket(optionValues, fail);
|
|
115
|
-
const historyWindow = resolveHistoryWindow(optionValues, fail);
|
|
116
|
-
const limit = parseIntegerOption({
|
|
117
|
-
value: optionValues.limit,
|
|
118
|
-
optionName: 'limit',
|
|
119
|
-
min: 1,
|
|
120
|
-
max: 10000,
|
|
121
|
-
defaultValue: 5000,
|
|
122
|
-
}, fail);
|
|
123
|
-
const response = await client.getProductHistory.mutate({
|
|
124
|
-
marketplaceId,
|
|
125
|
-
asin,
|
|
126
|
-
metrics,
|
|
127
|
-
format: 'agent',
|
|
128
|
-
bucket,
|
|
129
|
-
limit,
|
|
130
|
-
...historyWindow,
|
|
131
|
-
});
|
|
132
|
-
return buildCliHistoryResponse({
|
|
133
|
-
asin,
|
|
134
|
-
marketplaceId,
|
|
135
|
-
metrics,
|
|
136
|
-
response: response,
|
|
137
|
-
});
|
|
138
|
-
};
|
|
139
102
|
const resolveMetaCommand = (inputPositionals) => {
|
|
140
103
|
if (inputPositionals.length === 1 && inputPositionals[0] === 'changelog') {
|
|
141
104
|
return { name: 'changelog' };
|
|
@@ -144,7 +107,7 @@ const resolveMetaCommand = (inputPositionals) => {
|
|
|
144
107
|
};
|
|
145
108
|
const resolveCommand = (inputPositionals) => {
|
|
146
109
|
const [first, second, ...rest] = inputPositionals;
|
|
147
|
-
if (!first
|
|
110
|
+
if (!(first && second)) {
|
|
148
111
|
return null;
|
|
149
112
|
}
|
|
150
113
|
return {
|
|
@@ -161,9 +124,7 @@ const resolveCommandOrFail = (inputPositionals) => {
|
|
|
161
124
|
fail('UNKNOWN_COMMAND', 'Unknown command', { command: inputPositionals.join(' ') });
|
|
162
125
|
throw new Error('unreachable');
|
|
163
126
|
};
|
|
164
|
-
const isSupportedCommand = (command) => {
|
|
165
|
-
return SUPPORTED_COMMANDS.has(`${command.resource}:${command.verb}`);
|
|
166
|
-
};
|
|
127
|
+
const isSupportedCommand = (command) => SUPPORTED_COMMANDS.has(`${command.resource}:${command.verb}`);
|
|
167
128
|
await main().catch(error => {
|
|
168
129
|
const resolved = resolveError(error);
|
|
169
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,12 +15,14 @@ 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]',
|
|
24
26
|
' auth set --stdin',
|
|
25
27
|
' auth clear',
|
|
26
28
|
' config show',
|
|
@@ -36,13 +38,15 @@ export const printUsage = (paths) => {
|
|
|
36
38
|
' --version Print the CLI version',
|
|
37
39
|
' --baseUrl <origin> Override API origin',
|
|
38
40
|
` -m, --marketplace <id> Override marketplace (default: ${DEFAULT_MARKETPLACE_ID})`,
|
|
39
|
-
' --
|
|
40
|
-
' --metrics <list>
|
|
41
|
-
' --bucket <unit>
|
|
42
|
-
' --days <N>
|
|
43
|
-
' --
|
|
44
|
-
' --
|
|
45
|
-
' --
|
|
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)',
|
|
46
50
|
' --stdin Read auth secret from standard input',
|
|
47
51
|
'',
|
|
48
52
|
'FILES',
|
|
@@ -50,27 +54,26 @@ export const printUsage = (paths) => {
|
|
|
50
54
|
` ${paths.globalConfigPath} Global CLI settings`,
|
|
51
55
|
'',
|
|
52
56
|
'ENVIRONMENT',
|
|
53
|
-
` ${
|
|
57
|
+
` ${API_KEY_ENV_VAR} Merchbase API key override for CI/automation`,
|
|
54
58
|
' RR_API_URL API origin fallback',
|
|
55
59
|
' RR_MARKETPLACE_ID Marketplace fallback',
|
|
56
60
|
' RR_STORAGE_DIR Storage directory override',
|
|
57
|
-
'
|
|
58
|
-
' RR_ASINS Comma-separated ASIN fallback',
|
|
59
|
-
' RR_HISTORY_METRICS Comma-separated history metrics fallback',
|
|
61
|
+
' RR_HISTORY_METRICS Comma-separated product history metrics fallback',
|
|
60
62
|
'',
|
|
61
63
|
'EXAMPLES',
|
|
62
|
-
' rw auth set
|
|
63
|
-
' printf %s "$
|
|
64
|
-
` ${
|
|
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',
|
|
65
73
|
' RR_STORAGE_DIR=~/.config/rankwrangler rw config show',
|
|
66
74
|
' rw --version',
|
|
67
75
|
' rw changelog',
|
|
68
|
-
'
|
|
69
|
-
' rw config set storage-dir ~/.config/rankwrangler',
|
|
70
|
-
' rw config unset marketplace',
|
|
71
|
-
' rw products history B0DV53VS61 --metrics bsr,price --bucket week',
|
|
72
|
-
' rw products history B0DV53VS61 --startAt 2025-01-01 --endAt 2025-12-31 --bucket month',
|
|
73
|
-
' rankwrangler license status',
|
|
76
|
+
' rankwrangler auth status',
|
|
74
77
|
];
|
|
75
78
|
console.log(usage.join('\n'));
|
|
76
79
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rankwrangler/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Official RankWrangler command line interface",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
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": {
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"provenance": true
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@
|
|
27
|
+
"@merchbaseco/access": "0.3.0",
|
|
28
|
+
"@rankwrangler/http-client": "^0.9.0",
|
|
28
29
|
"keytar": "7.9.0"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|