@rankwrangler/cli 0.4.1 → 0.6.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 +25 -13
- package/dist/CHANGELOG.md +36 -0
- package/dist/cli-auth.d.ts +112 -0
- package/dist/cli-auth.js +222 -0
- package/dist/cli-config-command.d.ts +101 -0
- package/dist/cli-config-command.js +118 -0
- package/dist/cli-config.d.ts +8 -1
- package/dist/cli-config.js +20 -2
- package/dist/cli-options.d.ts +3 -2
- package/dist/cli-options.js +10 -4
- package/dist/cli-output.js +8 -0
- package/dist/history-response.d.ts +27 -4
- package/dist/history-response.js +5 -3
- package/dist/index.js +21 -60
- package/dist/usage.js +19 -6
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -11,21 +11,33 @@ npm install -g @rankwrangler/cli
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
export RR_LICENSE_KEY=rrk_...
|
|
15
14
|
rw --version
|
|
16
15
|
rw changelog
|
|
16
|
+
rw auth set rrk_...
|
|
17
|
+
printf %s "$RR_LICENSE_KEY" | rw auth set --stdin
|
|
17
18
|
rw config set storage-dir ~/.config/rankwrangler
|
|
19
|
+
rw config get marketplace
|
|
20
|
+
rw config unset marketplace
|
|
18
21
|
rw products get B0DV53VS61
|
|
19
|
-
rw products history B0DV53VS61 --metrics bsr,price
|
|
22
|
+
rw products history B0DV53VS61 --metrics bsr,price --bucket week
|
|
23
|
+
RR_LICENSE_KEY=rrk_... rw license status
|
|
20
24
|
# `rankwrangler` is also supported as an alias
|
|
21
25
|
```
|
|
22
26
|
|
|
23
|
-
`
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
29
41
|
|
|
30
42
|
## Development
|
|
31
43
|
|
|
@@ -55,9 +67,9 @@ matching `@rankwrangler/http-client` version is already available on npm.
|
|
|
55
67
|
|
|
56
68
|
```bash
|
|
57
69
|
cd packages/cli
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
set +a
|
|
61
|
-
npm whoami --userconfig ../../.npmrc
|
|
62
|
-
npm publish --access public --userconfig ../../.npmrc
|
|
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
|
|
63
72
|
```
|
|
73
|
+
|
|
74
|
+
On macOS, the helper reads `NPM_TOKEN` from the environment first and otherwise falls back to the
|
|
75
|
+
Keychain item `rankwrangler-npm-token` for account `$USER`.
|
package/dist/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## v0.6.0 - 2026-06-15
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add `rw auth set --stdin` and hidden local auth entry so agents can pipe license keys and humans
|
|
10
|
+
can avoid shell history.
|
|
11
|
+
- Add `rw config get`, `rw config unset`, and `rw config reset` for explicit non-secret config
|
|
12
|
+
inspection and cleanup.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Show auth source/status in `rw config show` without printing secrets.
|
|
17
|
+
- Update CLI help, README, CLI spec, and e2e coverage for the clearer auth/config surface.
|
|
18
|
+
|
|
19
|
+
### Removed
|
|
20
|
+
|
|
21
|
+
- Remove `rw config clear` in favor of `rw config unset <key>` and `rw config reset`.
|
|
22
|
+
|
|
23
|
+
## v0.5.0 - 2026-03-18
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Add CLI secure-store auth with `rw auth set`, `rw auth status`, and `rw auth clear`, storing
|
|
28
|
+
license keys in the platform secure store while keeping `RR_LICENSE_KEY` as an override for
|
|
29
|
+
CI, automation, and agent runtimes.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Change local development defaults so the app server no longer starts the job runner unless you
|
|
34
|
+
use the explicit jobs-enabled dev commands.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- Fix top search terms scheduler timestamp handling so due datasets are selected and refreshed
|
|
39
|
+
reliably.
|
|
40
|
+
|
|
5
41
|
## v0.4.1 - 2026-03-09
|
|
6
42
|
|
|
7
43
|
### Added
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export declare const LICENSE_KEY_ENV_VAR = "RR_LICENSE_KEY";
|
|
2
|
+
export declare const AUTH_SERVICE_NAME = "RankWrangler CLI";
|
|
3
|
+
export declare const AUTH_ACCOUNT_NAME = "license-key";
|
|
4
|
+
type CliFail = (code: string, message: string, details?: unknown) => never;
|
|
5
|
+
type AuthCommand = {
|
|
6
|
+
verb: string;
|
|
7
|
+
args: string[];
|
|
8
|
+
};
|
|
9
|
+
type AuthCommandOptions = {
|
|
10
|
+
stdin?: boolean;
|
|
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
|
+
export declare const resolveApiKey: () => Promise<string | undefined>;
|
|
18
|
+
export declare const runAuthCommand: (command: AuthCommand, fail: CliFail, options?: AuthCommandOptions) => Promise<{
|
|
19
|
+
source: string;
|
|
20
|
+
envOverride: boolean;
|
|
21
|
+
secureStore: {
|
|
22
|
+
backend: string;
|
|
23
|
+
serviceName: string;
|
|
24
|
+
accountName: string;
|
|
25
|
+
available: boolean;
|
|
26
|
+
hasStoredLicenseKey: boolean;
|
|
27
|
+
error?: undefined;
|
|
28
|
+
};
|
|
29
|
+
} | {
|
|
30
|
+
source: string;
|
|
31
|
+
envOverride: boolean;
|
|
32
|
+
secureStore: {
|
|
33
|
+
backend: string;
|
|
34
|
+
serviceName: string;
|
|
35
|
+
accountName: string;
|
|
36
|
+
available: boolean;
|
|
37
|
+
hasStoredLicenseKey: null;
|
|
38
|
+
error: string;
|
|
39
|
+
};
|
|
40
|
+
} | {
|
|
41
|
+
saved: boolean;
|
|
42
|
+
source: string;
|
|
43
|
+
envOverride: boolean;
|
|
44
|
+
secureStore: {
|
|
45
|
+
backend: string;
|
|
46
|
+
serviceName: string;
|
|
47
|
+
accountName: string;
|
|
48
|
+
available: boolean;
|
|
49
|
+
hasStoredLicenseKey: boolean;
|
|
50
|
+
error?: undefined;
|
|
51
|
+
};
|
|
52
|
+
} | {
|
|
53
|
+
saved: boolean;
|
|
54
|
+
source: string;
|
|
55
|
+
envOverride: boolean;
|
|
56
|
+
secureStore: {
|
|
57
|
+
backend: string;
|
|
58
|
+
serviceName: string;
|
|
59
|
+
accountName: string;
|
|
60
|
+
available: boolean;
|
|
61
|
+
hasStoredLicenseKey: null;
|
|
62
|
+
error: string;
|
|
63
|
+
};
|
|
64
|
+
} | {
|
|
65
|
+
cleared: boolean;
|
|
66
|
+
source: string;
|
|
67
|
+
envOverride: boolean;
|
|
68
|
+
secureStore: {
|
|
69
|
+
backend: string;
|
|
70
|
+
serviceName: string;
|
|
71
|
+
accountName: string;
|
|
72
|
+
available: boolean;
|
|
73
|
+
hasStoredLicenseKey: boolean;
|
|
74
|
+
error?: undefined;
|
|
75
|
+
};
|
|
76
|
+
} | {
|
|
77
|
+
cleared: boolean;
|
|
78
|
+
source: string;
|
|
79
|
+
envOverride: boolean;
|
|
80
|
+
secureStore: {
|
|
81
|
+
backend: string;
|
|
82
|
+
serviceName: string;
|
|
83
|
+
accountName: string;
|
|
84
|
+
available: boolean;
|
|
85
|
+
hasStoredLicenseKey: null;
|
|
86
|
+
error: string;
|
|
87
|
+
};
|
|
88
|
+
}>;
|
|
89
|
+
export declare const buildAuthStatus: () => Promise<{
|
|
90
|
+
source: string;
|
|
91
|
+
envOverride: boolean;
|
|
92
|
+
secureStore: {
|
|
93
|
+
backend: string;
|
|
94
|
+
serviceName: string;
|
|
95
|
+
accountName: string;
|
|
96
|
+
available: boolean;
|
|
97
|
+
hasStoredLicenseKey: boolean;
|
|
98
|
+
error?: undefined;
|
|
99
|
+
};
|
|
100
|
+
} | {
|
|
101
|
+
source: string;
|
|
102
|
+
envOverride: boolean;
|
|
103
|
+
secureStore: {
|
|
104
|
+
backend: string;
|
|
105
|
+
serviceName: string;
|
|
106
|
+
accountName: string;
|
|
107
|
+
available: boolean;
|
|
108
|
+
hasStoredLicenseKey: null;
|
|
109
|
+
error: string;
|
|
110
|
+
};
|
|
111
|
+
}>;
|
|
112
|
+
export {};
|
package/dist/cli-auth.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import readline from 'node:readline';
|
|
5
|
+
export const LICENSE_KEY_ENV_VAR = 'RR_LICENSE_KEY';
|
|
6
|
+
export const AUTH_SERVICE_NAME = 'RankWrangler CLI';
|
|
7
|
+
export const AUTH_ACCOUNT_NAME = 'license-key';
|
|
8
|
+
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
|
+
export const resolveApiKey = async () => {
|
|
20
|
+
const envLicenseKey = resolveEnvLicenseKey();
|
|
21
|
+
if (envLicenseKey) {
|
|
22
|
+
return envLicenseKey;
|
|
23
|
+
}
|
|
24
|
+
const secureStore = await createSecureStore();
|
|
25
|
+
return (await secureStore.get()) ?? undefined;
|
|
26
|
+
};
|
|
27
|
+
export const runAuthCommand = async (command, fail, options = {}) => {
|
|
28
|
+
if (command.verb === 'status') {
|
|
29
|
+
return buildAuthStatus();
|
|
30
|
+
}
|
|
31
|
+
if (command.verb === 'set') {
|
|
32
|
+
const value = await resolveAuthSetValue(command.args, fail, options);
|
|
33
|
+
const secureStore = await createSecureStore();
|
|
34
|
+
await secureStore.set(value);
|
|
35
|
+
return {
|
|
36
|
+
...(await buildAuthStatus()),
|
|
37
|
+
saved: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (command.verb === 'clear') {
|
|
41
|
+
const secureStore = await createSecureStore();
|
|
42
|
+
const cleared = await secureStore.clear();
|
|
43
|
+
return {
|
|
44
|
+
...(await buildAuthStatus()),
|
|
45
|
+
cleared,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
fail('UNKNOWN_COMMAND', 'Unknown auth command', { verb: command.verb });
|
|
49
|
+
};
|
|
50
|
+
export const buildAuthStatus = async () => {
|
|
51
|
+
const envLicenseKey = resolveEnvLicenseKey();
|
|
52
|
+
try {
|
|
53
|
+
const secureStore = await createSecureStore();
|
|
54
|
+
const storedLicenseKey = await secureStore.get();
|
|
55
|
+
return {
|
|
56
|
+
source: envLicenseKey ? 'env' : storedLicenseKey ? 'secure-store' : 'none',
|
|
57
|
+
envOverride: Boolean(envLicenseKey),
|
|
58
|
+
secureStore: {
|
|
59
|
+
backend: secureStore.backend,
|
|
60
|
+
serviceName: secureStore.serviceName,
|
|
61
|
+
accountName: secureStore.accountName,
|
|
62
|
+
available: true,
|
|
63
|
+
hasStoredLicenseKey: Boolean(storedLicenseKey),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return {
|
|
69
|
+
source: envLicenseKey ? 'env' : 'none',
|
|
70
|
+
envOverride: Boolean(envLicenseKey),
|
|
71
|
+
secureStore: {
|
|
72
|
+
backend: getSecureStoreBackendLabel(),
|
|
73
|
+
serviceName: AUTH_SERVICE_NAME,
|
|
74
|
+
accountName: AUTH_ACCOUNT_NAME,
|
|
75
|
+
available: false,
|
|
76
|
+
hasStoredLicenseKey: null,
|
|
77
|
+
error: error instanceof Error ? error.message : 'Unknown secure-store error',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const resolveAuthSetValue = async (args, fail, options) => {
|
|
83
|
+
const positionalValue = args.join(' ').trim();
|
|
84
|
+
if (positionalValue) {
|
|
85
|
+
return positionalValue;
|
|
86
|
+
}
|
|
87
|
+
if (options.stdin) {
|
|
88
|
+
const stdinValue = await readSecretFromStdin();
|
|
89
|
+
if (stdinValue) {
|
|
90
|
+
return stdinValue;
|
|
91
|
+
}
|
|
92
|
+
fail('INVALID_INPUT', 'auth set --stdin received an empty license key');
|
|
93
|
+
}
|
|
94
|
+
const envLicenseKey = resolveEnvLicenseKey();
|
|
95
|
+
if (envLicenseKey) {
|
|
96
|
+
return envLicenseKey;
|
|
97
|
+
}
|
|
98
|
+
if (process.stdin.isTTY && process.stderr.isTTY) {
|
|
99
|
+
const promptValue = await readSecretFromPrompt('RankWrangler license key: ');
|
|
100
|
+
if (promptValue) {
|
|
101
|
+
return promptValue;
|
|
102
|
+
}
|
|
103
|
+
fail('INVALID_INPUT', 'auth set received an empty license key');
|
|
104
|
+
}
|
|
105
|
+
fail('INVALID_INPUT', `auth set requires <licenseKey>, --stdin, or ${LICENSE_KEY_ENV_VAR}`);
|
|
106
|
+
};
|
|
107
|
+
const readSecretFromStdin = async () => {
|
|
108
|
+
const chunks = [];
|
|
109
|
+
for await (const chunk of process.stdin) {
|
|
110
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
111
|
+
}
|
|
112
|
+
return Buffer.concat(chunks).toString('utf8').trim();
|
|
113
|
+
};
|
|
114
|
+
const readSecretFromPrompt = async (prompt) => {
|
|
115
|
+
process.stderr.write(prompt);
|
|
116
|
+
setTerminalEcho(false);
|
|
117
|
+
const rl = readline.createInterface({
|
|
118
|
+
input: process.stdin,
|
|
119
|
+
output: process.stderr,
|
|
120
|
+
terminal: true,
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
const value = await new Promise(resolve => {
|
|
124
|
+
rl.question('', resolve);
|
|
125
|
+
});
|
|
126
|
+
process.stderr.write('\n');
|
|
127
|
+
return value.trim();
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
rl.close();
|
|
131
|
+
setTerminalEcho(true);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const setTerminalEcho = (enabled) => {
|
|
135
|
+
if (process.platform === 'win32' || !process.stdin.isTTY) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
spawnSync('stty', [enabled ? 'echo' : '-echo'], {
|
|
139
|
+
stdio: ['inherit', 'ignore', 'ignore'],
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
const resolveEnvLicenseKey = () => {
|
|
143
|
+
const value = process.env[LICENSE_KEY_ENV_VAR]?.trim();
|
|
144
|
+
return value ? value : undefined;
|
|
145
|
+
};
|
|
146
|
+
const createSecureStore = async () => {
|
|
147
|
+
const internalSecretStoreDir = process.env[INTERNAL_SECRET_STORE_DIR_ENV_VAR]?.trim();
|
|
148
|
+
if (internalSecretStoreDir) {
|
|
149
|
+
return createFileSecretStore(path.resolve(internalSecretStoreDir));
|
|
150
|
+
}
|
|
151
|
+
const keytar = await loadKeytar();
|
|
152
|
+
return {
|
|
153
|
+
backend: getSecureStoreBackendLabel(),
|
|
154
|
+
serviceName: AUTH_SERVICE_NAME,
|
|
155
|
+
accountName: AUTH_ACCOUNT_NAME,
|
|
156
|
+
get: () => keytar.getPassword(AUTH_SERVICE_NAME, AUTH_ACCOUNT_NAME),
|
|
157
|
+
set: value => keytar.setPassword(AUTH_SERVICE_NAME, AUTH_ACCOUNT_NAME, value),
|
|
158
|
+
clear: () => keytar.deletePassword(AUTH_SERVICE_NAME, AUTH_ACCOUNT_NAME),
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
const loadKeytar = async () => {
|
|
162
|
+
try {
|
|
163
|
+
const imported = (await import('keytar'));
|
|
164
|
+
return imported.default ?? imported;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
throw new CliAuthError('SECURE_STORE_UNAVAILABLE', 'platform secure store is unavailable', error instanceof Error ? { cause: error.message } : undefined);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const getSecureStoreBackendLabel = () => {
|
|
171
|
+
switch (process.platform) {
|
|
172
|
+
case 'darwin':
|
|
173
|
+
return 'macos-keychain';
|
|
174
|
+
case 'win32':
|
|
175
|
+
return 'windows-credential-manager';
|
|
176
|
+
default:
|
|
177
|
+
return 'platform-secure-store';
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const createFileSecretStore = (secretStoreDir) => {
|
|
181
|
+
const secretPath = path.join(secretStoreDir, 'license-key.json');
|
|
182
|
+
return {
|
|
183
|
+
backend: 'test-file-store',
|
|
184
|
+
serviceName: AUTH_SERVICE_NAME,
|
|
185
|
+
accountName: AUTH_ACCOUNT_NAME,
|
|
186
|
+
get: async () => {
|
|
187
|
+
try {
|
|
188
|
+
const raw = await readFile(secretPath, 'utf8');
|
|
189
|
+
const parsed = JSON.parse(raw);
|
|
190
|
+
return typeof parsed.licenseKey === 'string' ? parsed.licenseKey : null;
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (error &&
|
|
194
|
+
typeof error === 'object' &&
|
|
195
|
+
'code' in error &&
|
|
196
|
+
error.code === 'ENOENT') {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
throw new CliAuthError('INVALID_CONFIG', 'failed to read secure-store test data');
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
set: async (value) => {
|
|
203
|
+
await mkdir(secretStoreDir, { recursive: true });
|
|
204
|
+
await writeFile(secretPath, `${JSON.stringify({ licenseKey: value }, null, 2)}\n`, 'utf8');
|
|
205
|
+
},
|
|
206
|
+
clear: async () => {
|
|
207
|
+
try {
|
|
208
|
+
await rm(secretPath);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (error &&
|
|
213
|
+
typeof error === 'object' &&
|
|
214
|
+
'code' in error &&
|
|
215
|
+
error.code === 'ENOENT') {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type CliConfig, type CliPaths } from './cli-config';
|
|
2
|
+
type CliFail = (code: string, message: string, details?: unknown) => never;
|
|
3
|
+
type ConfigCommand = {
|
|
4
|
+
verb: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
};
|
|
7
|
+
export declare const runConfigCommand: (command: ConfigCommand, config: CliConfig, paths: CliPaths, fail: CliFail) => Promise<{
|
|
8
|
+
storageDir: string;
|
|
9
|
+
path: string;
|
|
10
|
+
globalPath: string;
|
|
11
|
+
config: CliConfig;
|
|
12
|
+
auth: {
|
|
13
|
+
source: string;
|
|
14
|
+
envOverride: boolean;
|
|
15
|
+
secureStore: {
|
|
16
|
+
backend: string;
|
|
17
|
+
serviceName: string;
|
|
18
|
+
accountName: string;
|
|
19
|
+
available: boolean;
|
|
20
|
+
hasStoredLicenseKey: boolean;
|
|
21
|
+
error?: undefined;
|
|
22
|
+
};
|
|
23
|
+
} | {
|
|
24
|
+
source: string;
|
|
25
|
+
envOverride: boolean;
|
|
26
|
+
secureStore: {
|
|
27
|
+
backend: string;
|
|
28
|
+
serviceName: string;
|
|
29
|
+
accountName: string;
|
|
30
|
+
available: boolean;
|
|
31
|
+
hasStoredLicenseKey: null;
|
|
32
|
+
error: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
} | {
|
|
36
|
+
key: "base-url" | "marketplace" | "storage-dir";
|
|
37
|
+
value: string | null;
|
|
38
|
+
} | {
|
|
39
|
+
unset: "base-url" | "marketplace" | "storage-dir";
|
|
40
|
+
storageDir: string;
|
|
41
|
+
path: string;
|
|
42
|
+
globalPath: string;
|
|
43
|
+
config: CliConfig;
|
|
44
|
+
auth: {
|
|
45
|
+
source: string;
|
|
46
|
+
envOverride: boolean;
|
|
47
|
+
secureStore: {
|
|
48
|
+
backend: string;
|
|
49
|
+
serviceName: string;
|
|
50
|
+
accountName: string;
|
|
51
|
+
available: boolean;
|
|
52
|
+
hasStoredLicenseKey: boolean;
|
|
53
|
+
error?: undefined;
|
|
54
|
+
};
|
|
55
|
+
} | {
|
|
56
|
+
source: string;
|
|
57
|
+
envOverride: boolean;
|
|
58
|
+
secureStore: {
|
|
59
|
+
backend: string;
|
|
60
|
+
serviceName: string;
|
|
61
|
+
accountName: string;
|
|
62
|
+
available: boolean;
|
|
63
|
+
hasStoredLicenseKey: null;
|
|
64
|
+
error: string;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
key?: undefined;
|
|
68
|
+
value?: undefined;
|
|
69
|
+
} | {
|
|
70
|
+
reset: boolean;
|
|
71
|
+
storageDir: string;
|
|
72
|
+
path: string;
|
|
73
|
+
globalPath: string;
|
|
74
|
+
config: CliConfig;
|
|
75
|
+
auth: {
|
|
76
|
+
source: string;
|
|
77
|
+
envOverride: boolean;
|
|
78
|
+
secureStore: {
|
|
79
|
+
backend: string;
|
|
80
|
+
serviceName: string;
|
|
81
|
+
accountName: string;
|
|
82
|
+
available: boolean;
|
|
83
|
+
hasStoredLicenseKey: boolean;
|
|
84
|
+
error?: undefined;
|
|
85
|
+
};
|
|
86
|
+
} | {
|
|
87
|
+
source: string;
|
|
88
|
+
envOverride: boolean;
|
|
89
|
+
secureStore: {
|
|
90
|
+
backend: string;
|
|
91
|
+
serviceName: string;
|
|
92
|
+
accountName: string;
|
|
93
|
+
available: boolean;
|
|
94
|
+
hasStoredLicenseKey: null;
|
|
95
|
+
error: string;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
key?: undefined;
|
|
99
|
+
value?: undefined;
|
|
100
|
+
}>;
|
|
101
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { buildAuthStatus } from './cli-auth.js';
|
|
2
|
+
import { resetConfig, saveConfig, switchStorageDir, unsetStorageDir, } from './cli-config.js';
|
|
3
|
+
import { normalizeBaseUrl } from './cli-options.js';
|
|
4
|
+
const CONFIG_KEYS = ['base-url', 'marketplace', 'storage-dir'];
|
|
5
|
+
export const runConfigCommand = async (command, config, paths, fail) => {
|
|
6
|
+
if (command.verb === 'show') {
|
|
7
|
+
return buildConfigResponse(paths, config);
|
|
8
|
+
}
|
|
9
|
+
if (command.verb === 'get') {
|
|
10
|
+
const key = requireConfigKey(command.args[0], 'config get', fail);
|
|
11
|
+
return {
|
|
12
|
+
key,
|
|
13
|
+
value: getConfigValue(key, paths, config),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
if (command.verb === 'unset') {
|
|
17
|
+
const key = requireConfigKey(command.args[0], 'config unset', fail);
|
|
18
|
+
const nextState = await unsetConfigValue(key, paths, config);
|
|
19
|
+
return {
|
|
20
|
+
...(await buildConfigResponse(nextState.paths, nextState.config)),
|
|
21
|
+
unset: key,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (command.verb === 'reset') {
|
|
25
|
+
const nextState = await resetConfig(paths);
|
|
26
|
+
return {
|
|
27
|
+
...(await buildConfigResponse(nextState.paths, nextState.config)),
|
|
28
|
+
reset: true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (command.verb !== 'set') {
|
|
32
|
+
fail('UNKNOWN_COMMAND', 'Unknown config command', { verb: command.verb });
|
|
33
|
+
}
|
|
34
|
+
const [key, ...valueParts] = command.args;
|
|
35
|
+
if (!key || valueParts.length === 0) {
|
|
36
|
+
fail('INVALID_INPUT', 'config set requires <key> <value>');
|
|
37
|
+
}
|
|
38
|
+
const value = valueParts.join(' ').trim();
|
|
39
|
+
if (!value) {
|
|
40
|
+
fail('INVALID_INPUT', 'config set value cannot be empty');
|
|
41
|
+
}
|
|
42
|
+
if (key === 'storage-dir') {
|
|
43
|
+
const nextState = await switchStorageDir({
|
|
44
|
+
currentConfig: config,
|
|
45
|
+
currentPaths: paths,
|
|
46
|
+
requestedStorageDir: value,
|
|
47
|
+
});
|
|
48
|
+
return buildConfigResponse(nextState.paths, nextState.config);
|
|
49
|
+
}
|
|
50
|
+
const nextConfig = { ...config };
|
|
51
|
+
if (key === 'base-url') {
|
|
52
|
+
nextConfig.baseUrl = normalizeBaseUrl(value, fail);
|
|
53
|
+
}
|
|
54
|
+
else if (key === 'marketplace') {
|
|
55
|
+
nextConfig.marketplaceId = value;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
failUnsupportedConfigKey(key, fail);
|
|
59
|
+
}
|
|
60
|
+
await saveConfig(paths, nextConfig);
|
|
61
|
+
return buildConfigResponse(paths, nextConfig);
|
|
62
|
+
};
|
|
63
|
+
const buildConfigResponse = async (paths, config) => {
|
|
64
|
+
return {
|
|
65
|
+
storageDir: paths.storageDir,
|
|
66
|
+
path: paths.configPath,
|
|
67
|
+
globalPath: paths.globalConfigPath,
|
|
68
|
+
config,
|
|
69
|
+
auth: await buildAuthStatus(),
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const requireConfigKey = (key, commandName, fail) => {
|
|
73
|
+
if (!key) {
|
|
74
|
+
fail('INVALID_INPUT', `${commandName} requires <key>`, {
|
|
75
|
+
supportedKeys: CONFIG_KEYS,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (isConfigKey(key)) {
|
|
79
|
+
return key;
|
|
80
|
+
}
|
|
81
|
+
return failUnsupportedConfigKey(key, fail);
|
|
82
|
+
};
|
|
83
|
+
const isConfigKey = (key) => {
|
|
84
|
+
return CONFIG_KEYS.includes(key);
|
|
85
|
+
};
|
|
86
|
+
const getConfigValue = (key, paths, config) => {
|
|
87
|
+
switch (key) {
|
|
88
|
+
case 'base-url':
|
|
89
|
+
return config.baseUrl ?? null;
|
|
90
|
+
case 'marketplace':
|
|
91
|
+
return config.marketplaceId ?? null;
|
|
92
|
+
case 'storage-dir':
|
|
93
|
+
return paths.storageDir;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const unsetConfigValue = async (key, paths, config) => {
|
|
97
|
+
if (key === 'storage-dir') {
|
|
98
|
+
return unsetStorageDir(paths);
|
|
99
|
+
}
|
|
100
|
+
const nextConfig = { ...config };
|
|
101
|
+
if (key === 'base-url') {
|
|
102
|
+
nextConfig.baseUrl = undefined;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
nextConfig.marketplaceId = undefined;
|
|
106
|
+
}
|
|
107
|
+
await saveConfig(paths, nextConfig);
|
|
108
|
+
return {
|
|
109
|
+
paths,
|
|
110
|
+
config: nextConfig,
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
const failUnsupportedConfigKey = (key, fail) => {
|
|
114
|
+
fail('INVALID_INPUT', 'unsupported config key', {
|
|
115
|
+
key,
|
|
116
|
+
supportedKeys: CONFIG_KEYS,
|
|
117
|
+
});
|
|
118
|
+
};
|
package/dist/cli-config.d.ts
CHANGED
|
@@ -19,8 +19,15 @@ export declare const loadCliContext: () => Promise<{
|
|
|
19
19
|
config: CliConfig;
|
|
20
20
|
}>;
|
|
21
21
|
export declare const loadCliPathsOrDefault: () => Promise<CliPaths>;
|
|
22
|
-
export declare const
|
|
22
|
+
export declare const resetConfig: (paths: CliPaths) => Promise<{
|
|
23
|
+
paths: CliPaths;
|
|
24
|
+
config: {};
|
|
25
|
+
}>;
|
|
23
26
|
export declare const saveConfig: (paths: CliPaths, config: CliConfig) => Promise<void>;
|
|
27
|
+
export declare const unsetStorageDir: (paths: CliPaths) => Promise<{
|
|
28
|
+
paths: CliPaths;
|
|
29
|
+
config: CliConfig;
|
|
30
|
+
}>;
|
|
24
31
|
export declare const switchStorageDir: ({ currentConfig, currentPaths, requestedStorageDir, }: {
|
|
25
32
|
currentConfig: CliConfig;
|
|
26
33
|
currentPaths: CliPaths;
|
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);
|
package/dist/cli-options.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CliConfig } from './cli-config';
|
|
2
|
-
import { type HistoryMetricAlias } from './history-response';
|
|
2
|
+
import { type HistoryBucket, type HistoryMetricAlias } from './history-response';
|
|
3
3
|
type CliFail = (code: string, message: string, details?: unknown) => never;
|
|
4
4
|
export type CliOptionValues = {
|
|
5
5
|
baseUrl?: string;
|
|
@@ -10,6 +10,7 @@ export type CliOptionValues = {
|
|
|
10
10
|
endAt?: string;
|
|
11
11
|
days?: string;
|
|
12
12
|
limit?: string;
|
|
13
|
+
bucket?: string;
|
|
13
14
|
help?: boolean;
|
|
14
15
|
version?: boolean;
|
|
15
16
|
};
|
|
@@ -17,12 +18,12 @@ export declare const requireMarketplaceId: (values: CliOptionValues, config: Cli
|
|
|
17
18
|
export declare const requireAsins: (commandArgs: string[], values: CliOptionValues, fail: CliFail) => string[];
|
|
18
19
|
export declare const requireSingleAsin: (commandArgs: string[], values: CliOptionValues, fail: CliFail) => string;
|
|
19
20
|
export declare const resolveHistoryMetrics: (values: CliOptionValues, fail: CliFail) => HistoryMetricAlias[];
|
|
21
|
+
export declare const resolveHistoryBucket: (values: CliOptionValues, fail: CliFail) => HistoryBucket;
|
|
20
22
|
export declare const resolveHistoryWindow: (values: CliOptionValues, fail: CliFail) => {
|
|
21
23
|
days: number;
|
|
22
24
|
endAt?: string | undefined;
|
|
23
25
|
startAt?: string | undefined;
|
|
24
26
|
};
|
|
25
|
-
export declare const resolveApiKey: () => string | undefined;
|
|
26
27
|
export declare const resolveBaseUrl: (values: CliOptionValues, config: CliConfig, defaultBaseUrl: string, fail: CliFail) => string;
|
|
27
28
|
export declare const normalizeBaseUrl: (value: string, fail: CliFail) => string;
|
|
28
29
|
export declare const parseIntegerOption: ({ value, optionName, min, max, defaultValue, }: {
|
package/dist/cli-options.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HISTORY_METRIC_ALIASES } from './history-response.js';
|
|
1
|
+
import { HISTORY_BUCKETS, HISTORY_METRIC_ALIASES, } from './history-response.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;
|
|
@@ -38,6 +38,15 @@ export const resolveHistoryMetrics = (values, fail) => {
|
|
|
38
38
|
}
|
|
39
39
|
return Array.from(new Set(requested));
|
|
40
40
|
};
|
|
41
|
+
export const resolveHistoryBucket = (values, fail) => {
|
|
42
|
+
const requested = (values.bucket ?? 'auto').trim().toLowerCase();
|
|
43
|
+
if (!HISTORY_BUCKETS.includes(requested)) {
|
|
44
|
+
fail('INVALID_INPUT', `unsupported history bucket: ${requested}`, {
|
|
45
|
+
supportedBuckets: HISTORY_BUCKETS,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return requested;
|
|
49
|
+
};
|
|
41
50
|
export const resolveHistoryWindow = (values, fail) => {
|
|
42
51
|
const startAt = values.startAt ? normalizeDateOption('startAt', values.startAt, fail) : undefined;
|
|
43
52
|
const endAt = values.endAt ? normalizeDateOption('endAt', values.endAt, fail) : undefined;
|
|
@@ -56,9 +65,6 @@ export const resolveHistoryWindow = (values, fail) => {
|
|
|
56
65
|
}, fail),
|
|
57
66
|
};
|
|
58
67
|
};
|
|
59
|
-
export const resolveApiKey = () => {
|
|
60
|
-
return process.env.RR_LICENSE_KEY;
|
|
61
|
-
};
|
|
62
68
|
export const resolveBaseUrl = (values, config, defaultBaseUrl, fail) => {
|
|
63
69
|
return normalizeBaseUrl(values.baseUrl ?? config.baseUrl ?? process.env.RR_API_URL ?? defaultBaseUrl, fail);
|
|
64
70
|
};
|
package/dist/cli-output.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CliAuthError } from './cli-auth.js';
|
|
1
2
|
import { CliStorageError } from './cli-config.js';
|
|
2
3
|
const OUTPUT_PRETTY = true;
|
|
3
4
|
export const printSuccess = (data) => {
|
|
@@ -27,6 +28,13 @@ export const resolveError = (error) => {
|
|
|
27
28
|
details: error.details,
|
|
28
29
|
};
|
|
29
30
|
}
|
|
31
|
+
if (error instanceof CliAuthError) {
|
|
32
|
+
return {
|
|
33
|
+
code: error.code,
|
|
34
|
+
message: error.message,
|
|
35
|
+
details: error.details,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
30
38
|
if (error && typeof error === 'object' && 'data' in error) {
|
|
31
39
|
const data = error.data;
|
|
32
40
|
if (data?.code) {
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
export declare const HISTORY_METRIC_ALIASES: readonly ["bsr", "price"];
|
|
2
|
+
export declare const HISTORY_BUCKETS: readonly ["auto", "day", "week", "month"];
|
|
2
3
|
export type HistoryMetricAlias = (typeof HISTORY_METRIC_ALIASES)[number];
|
|
3
|
-
type
|
|
4
|
+
export type HistoryBucket = (typeof HISTORY_BUCKETS)[number];
|
|
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
|
+
};
|
|
4
15
|
export type AgentHistorySeries = {
|
|
5
16
|
bsr?: {
|
|
6
17
|
unit: 'rank';
|
|
@@ -8,13 +19,15 @@ export type AgentHistorySeries = {
|
|
|
8
19
|
id: number;
|
|
9
20
|
name: string | null;
|
|
10
21
|
} | null;
|
|
11
|
-
|
|
22
|
+
buckets: HistoryBucketTuple[];
|
|
23
|
+
summary: HistoryBucketSummary;
|
|
12
24
|
};
|
|
13
25
|
price?: {
|
|
14
26
|
unit: 'minorCurrency';
|
|
15
27
|
currencyCode: 'USD';
|
|
16
28
|
valueScale: 100;
|
|
17
|
-
|
|
29
|
+
buckets: HistoryBucketTuple[];
|
|
30
|
+
summary: HistoryBucketSummary;
|
|
18
31
|
};
|
|
19
32
|
};
|
|
20
33
|
export type AgentHistoryResponse = {
|
|
@@ -22,6 +35,11 @@ export type AgentHistoryResponse = {
|
|
|
22
35
|
status?: string;
|
|
23
36
|
syncTriggered: boolean;
|
|
24
37
|
latestImportAt: string | null;
|
|
38
|
+
range?: {
|
|
39
|
+
startAt: string;
|
|
40
|
+
endAt: string;
|
|
41
|
+
bucket: Exclude<HistoryBucket, 'auto'>;
|
|
42
|
+
};
|
|
25
43
|
series?: AgentHistorySeries;
|
|
26
44
|
};
|
|
27
45
|
export declare const buildCliHistoryResponse: ({ asin, marketplaceId, metrics, response, }: {
|
|
@@ -30,12 +48,17 @@ export declare const buildCliHistoryResponse: ({ asin, marketplaceId, metrics, r
|
|
|
30
48
|
metrics: HistoryMetricAlias[];
|
|
31
49
|
response: AgentHistoryResponse;
|
|
32
50
|
}) => {
|
|
51
|
+
series: AgentHistorySeries;
|
|
52
|
+
range?: {
|
|
53
|
+
startAt: string;
|
|
54
|
+
endAt: string;
|
|
55
|
+
bucket: Exclude<HistoryBucket, "auto">;
|
|
56
|
+
} | undefined;
|
|
33
57
|
schemaVersion: number;
|
|
34
58
|
asin: string;
|
|
35
59
|
marketplaceId: string;
|
|
36
60
|
status: string;
|
|
37
61
|
latestImportAt: string | null;
|
|
38
62
|
syncTriggered: boolean;
|
|
39
|
-
series: AgentHistorySeries;
|
|
40
63
|
};
|
|
41
64
|
export {};
|
package/dist/history-response.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const HISTORY_METRIC_ALIASES = ['bsr', 'price'];
|
|
2
|
+
export const HISTORY_BUCKETS = ['auto', 'day', 'week', 'month'];
|
|
2
3
|
export const buildCliHistoryResponse = ({ asin, marketplaceId, metrics, response, }) => {
|
|
3
4
|
const series = {
|
|
4
5
|
...(metrics.includes('bsr') && response.series?.bsr ? { bsr: response.series.bsr } : {}),
|
|
@@ -6,15 +7,16 @@ export const buildCliHistoryResponse = ({ asin, marketplaceId, metrics, response
|
|
|
6
7
|
? { price: response.series.price }
|
|
7
8
|
: {}),
|
|
8
9
|
};
|
|
9
|
-
const
|
|
10
|
+
const hasAnyBuckets = Object.values(series).some(metricSeries => metricSeries.buckets.length > 0);
|
|
10
11
|
const status = normalizeHistoryStatus(response.status);
|
|
11
12
|
return {
|
|
12
|
-
schemaVersion: response.schemaVersion ??
|
|
13
|
+
schemaVersion: response.schemaVersion ?? 2,
|
|
13
14
|
asin,
|
|
14
15
|
marketplaceId,
|
|
15
|
-
status: status ?? (response.syncTriggered ? 'collecting' :
|
|
16
|
+
status: status ?? (response.syncTriggered ? 'collecting' : hasAnyBuckets ? 'ready' : 'empty'),
|
|
16
17
|
latestImportAt: response.latestImportAt,
|
|
17
18
|
syncTriggered: response.syncTriggered,
|
|
19
|
+
...(response.range ? { range: response.range } : {}),
|
|
18
20
|
series,
|
|
19
21
|
};
|
|
20
22
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
3
|
import { createRankWranglerClient, DEFAULT_API_BASE_URL } from '@rankwrangler/http-client';
|
|
4
|
+
import { resolveApiKey, runAuthCommand } from './cli-auth.js';
|
|
4
5
|
import { printBundledChangelog, printCliVersion } from './cli-metadata.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
6
|
+
import { runConfigCommand } from './cli-config-command.js';
|
|
7
|
+
import { loadCliContext, loadCliPathsOrDefault } from './cli-config.js';
|
|
8
|
+
import { parseIntegerOption, requireAsins, requireMarketplaceId, requireSingleAsin, resolveBaseUrl, resolveHistoryBucket, resolveHistoryMetrics, resolveHistoryWindow, } from './cli-options.js';
|
|
7
9
|
import { fail, printSuccess, resolveError } from './cli-output.js';
|
|
8
10
|
import { buildCliHistoryResponse } from './history-response.js';
|
|
9
11
|
import { printUsage } from './usage.js';
|
|
@@ -12,8 +14,13 @@ const SUPPORTED_COMMANDS = new Set([
|
|
|
12
14
|
'products:history',
|
|
13
15
|
'license:status',
|
|
14
16
|
'license:validate',
|
|
17
|
+
'auth:status',
|
|
18
|
+
'auth:set',
|
|
19
|
+
'auth:clear',
|
|
15
20
|
'config:show',
|
|
16
|
-
'config:
|
|
21
|
+
'config:get',
|
|
22
|
+
'config:unset',
|
|
23
|
+
'config:reset',
|
|
17
24
|
'config:set',
|
|
18
25
|
]);
|
|
19
26
|
const { positionals, values } = parseArgs({
|
|
@@ -29,6 +36,8 @@ const { positionals, values } = parseArgs({
|
|
|
29
36
|
endAt: { type: 'string' },
|
|
30
37
|
days: { type: 'string' },
|
|
31
38
|
limit: { type: 'string' },
|
|
39
|
+
bucket: { type: 'string' },
|
|
40
|
+
stdin: { type: 'boolean' },
|
|
32
41
|
},
|
|
33
42
|
allowPositionals: true,
|
|
34
43
|
});
|
|
@@ -57,12 +66,16 @@ const main = async () => {
|
|
|
57
66
|
}
|
|
58
67
|
const { config, paths } = await loadCliContext();
|
|
59
68
|
if (command.resource === 'config') {
|
|
60
|
-
printSuccess(await runConfigCommand(command, config, paths));
|
|
69
|
+
printSuccess(await runConfigCommand(command, config, paths, fail));
|
|
61
70
|
return;
|
|
62
71
|
}
|
|
63
|
-
|
|
72
|
+
if (command.resource === 'auth') {
|
|
73
|
+
printSuccess(await runAuthCommand(command, fail, { stdin: Boolean(values.stdin) }));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const apiKey = await resolveApiKey();
|
|
64
77
|
if (!apiKey) {
|
|
65
|
-
fail('MISSING_CONFIG', '
|
|
78
|
+
fail('MISSING_CONFIG', 'license key is required. run `rw auth set`, `rw auth set --stdin`, or set RR_LICENSE_KEY');
|
|
66
79
|
}
|
|
67
80
|
const client = createRankWranglerClient({
|
|
68
81
|
baseUrl: resolveBaseUrl(optionValues, config, DEFAULT_API_BASE_URL, fail),
|
|
@@ -98,6 +111,7 @@ const runProductHistoryCommand = async (command, client, config) => {
|
|
|
98
111
|
const asin = requireSingleAsin(command.args, optionValues, fail);
|
|
99
112
|
const marketplaceId = requireMarketplaceId(optionValues, config);
|
|
100
113
|
const metrics = resolveHistoryMetrics(optionValues, fail);
|
|
114
|
+
const bucket = resolveHistoryBucket(optionValues, fail);
|
|
101
115
|
const historyWindow = resolveHistoryWindow(optionValues, fail);
|
|
102
116
|
const limit = parseIntegerOption({
|
|
103
117
|
value: optionValues.limit,
|
|
@@ -111,6 +125,7 @@ const runProductHistoryCommand = async (command, client, config) => {
|
|
|
111
125
|
asin,
|
|
112
126
|
metrics,
|
|
113
127
|
format: 'agent',
|
|
128
|
+
bucket,
|
|
114
129
|
limit,
|
|
115
130
|
...historyWindow,
|
|
116
131
|
});
|
|
@@ -121,60 +136,6 @@ const runProductHistoryCommand = async (command, client, config) => {
|
|
|
121
136
|
response: response,
|
|
122
137
|
});
|
|
123
138
|
};
|
|
124
|
-
const runConfigCommand = async (command, config, paths) => {
|
|
125
|
-
if (command.verb === 'show') {
|
|
126
|
-
return buildConfigResponse(paths, config);
|
|
127
|
-
}
|
|
128
|
-
if (command.verb === 'clear') {
|
|
129
|
-
await clearConfig(paths);
|
|
130
|
-
return {
|
|
131
|
-
...buildConfigResponse(paths, {}),
|
|
132
|
-
cleared: true,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
if (command.verb !== 'set') {
|
|
136
|
-
fail('UNKNOWN_COMMAND', 'Unknown config command', { verb: command.verb });
|
|
137
|
-
}
|
|
138
|
-
const [key, ...valueParts] = command.args;
|
|
139
|
-
if (!key || valueParts.length === 0) {
|
|
140
|
-
fail('INVALID_INPUT', 'config set requires <key> <value>');
|
|
141
|
-
}
|
|
142
|
-
const value = valueParts.join(' ').trim();
|
|
143
|
-
if (!value) {
|
|
144
|
-
fail('INVALID_INPUT', 'config set value cannot be empty');
|
|
145
|
-
}
|
|
146
|
-
if (key === 'storage-dir') {
|
|
147
|
-
const nextState = await switchStorageDir({
|
|
148
|
-
currentConfig: config,
|
|
149
|
-
currentPaths: paths,
|
|
150
|
-
requestedStorageDir: value,
|
|
151
|
-
});
|
|
152
|
-
return buildConfigResponse(nextState.paths, nextState.config);
|
|
153
|
-
}
|
|
154
|
-
const nextConfig = { ...config };
|
|
155
|
-
if (key === 'base-url') {
|
|
156
|
-
nextConfig.baseUrl = normalizeBaseUrl(value, fail);
|
|
157
|
-
}
|
|
158
|
-
else if (key === 'marketplace') {
|
|
159
|
-
nextConfig.marketplaceId = value;
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
fail('INVALID_INPUT', 'unsupported config key', {
|
|
163
|
-
key,
|
|
164
|
-
supportedKeys: ['base-url', 'marketplace', 'storage-dir'],
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
await saveConfig(paths, nextConfig);
|
|
168
|
-
return buildConfigResponse(paths, nextConfig);
|
|
169
|
-
};
|
|
170
|
-
const buildConfigResponse = (paths, config) => {
|
|
171
|
-
return {
|
|
172
|
-
storageDir: paths.storageDir,
|
|
173
|
-
path: paths.configPath,
|
|
174
|
-
globalPath: paths.globalConfigPath,
|
|
175
|
-
config,
|
|
176
|
-
};
|
|
177
|
-
};
|
|
178
139
|
const resolveMetaCommand = (inputPositionals) => {
|
|
179
140
|
if (inputPositionals.length === 1 && inputPositionals[0] === 'changelog') {
|
|
180
141
|
return { name: 'changelog' };
|
package/dist/usage.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LICENSE_KEY_ENV_VAR } from './cli-auth.js';
|
|
1
2
|
const DEFAULT_MARKETPLACE_ID = 'ATVPDKIKX0DER';
|
|
2
3
|
export const printUsage = (paths) => {
|
|
3
4
|
const usage = [
|
|
@@ -18,11 +19,17 @@ export const printUsage = (paths) => {
|
|
|
18
19
|
' products history <ASIN>',
|
|
19
20
|
' license status',
|
|
20
21
|
' license validate',
|
|
22
|
+
' auth status',
|
|
23
|
+
' auth set [licenseKey]',
|
|
24
|
+
' auth set --stdin',
|
|
25
|
+
' auth clear',
|
|
21
26
|
' config show',
|
|
22
|
-
' config
|
|
27
|
+
' config get <key>',
|
|
23
28
|
' config set base-url <origin>',
|
|
24
29
|
' config set marketplace <marketplaceId>',
|
|
25
30
|
' config set storage-dir <path>',
|
|
31
|
+
' config unset <key>',
|
|
32
|
+
' config reset',
|
|
26
33
|
'',
|
|
27
34
|
'OPTIONS',
|
|
28
35
|
' -h, --help Show this help message',
|
|
@@ -31,17 +38,19 @@ export const printUsage = (paths) => {
|
|
|
31
38
|
` -m, --marketplace <id> Override marketplace (default: ${DEFAULT_MARKETPLACE_ID})`,
|
|
32
39
|
' --asin <ASIN> Add ASIN (repeatable)',
|
|
33
40
|
' --metrics <list> History metrics: bsr,price (default: bsr,price)',
|
|
41
|
+
' --bucket <unit> History buckets: auto,day,week,month (default: auto)',
|
|
34
42
|
' --days <N> History lookback window (30-3650, default: 365)',
|
|
35
43
|
' --startAt <ISO> History range start (ISO date/time)',
|
|
36
44
|
' --endAt <ISO> History range end (ISO date/time)',
|
|
37
|
-
' --limit <N>
|
|
45
|
+
' --limit <N> Internal point cap (1-10000, default: 5000)',
|
|
46
|
+
' --stdin Read auth secret from standard input',
|
|
38
47
|
'',
|
|
39
48
|
'FILES',
|
|
40
49
|
` ${paths.configPath} Active CLI config`,
|
|
41
50
|
` ${paths.globalConfigPath} Global CLI settings`,
|
|
42
51
|
'',
|
|
43
52
|
'ENVIRONMENT',
|
|
44
|
-
|
|
53
|
+
` ${LICENSE_KEY_ENV_VAR} License key override for CI/automation`,
|
|
45
54
|
' RR_API_URL API origin fallback',
|
|
46
55
|
' RR_MARKETPLACE_ID Marketplace fallback',
|
|
47
56
|
' RR_STORAGE_DIR Storage directory override',
|
|
@@ -50,13 +59,17 @@ export const printUsage = (paths) => {
|
|
|
50
59
|
' RR_HISTORY_METRICS Comma-separated history metrics fallback',
|
|
51
60
|
'',
|
|
52
61
|
'EXAMPLES',
|
|
53
|
-
'
|
|
62
|
+
' rw auth set rrk_...',
|
|
63
|
+
' printf %s "$RR_LICENSE_KEY" | rw auth set --stdin',
|
|
64
|
+
` ${LICENSE_KEY_ENV_VAR}=rrk_... rw products get B0DV53VS61`,
|
|
54
65
|
' RR_STORAGE_DIR=~/.config/rankwrangler rw config show',
|
|
55
66
|
' rw --version',
|
|
56
67
|
' rw changelog',
|
|
68
|
+
' rw config get marketplace',
|
|
57
69
|
' rw config set storage-dir ~/.config/rankwrangler',
|
|
58
|
-
' rw
|
|
59
|
-
' rw products history B0DV53VS61 --
|
|
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',
|
|
60
73
|
' rankwrangler license status',
|
|
61
74
|
];
|
|
62
75
|
console.log(usage.join('\n'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rankwrangler/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Official RankWrangler command line interface",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,14 +20,16 @@
|
|
|
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
|
-
"@rankwrangler/http-client": "^0.
|
|
27
|
+
"@rankwrangler/http-client": "^0.6.0",
|
|
28
|
+
"keytar": "7.9.0"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
|
-
"@types/node": "
|
|
30
|
-
"typescript": "
|
|
31
|
+
"@types/node": "24.10.12",
|
|
32
|
+
"typescript": "5.9.3"
|
|
31
33
|
},
|
|
32
34
|
"packageManager": "bun@1.1.27"
|
|
33
35
|
}
|