@rankwrangler/cli 0.5.0 → 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 +19 -12
- package/dist/CHANGELOG.md +18 -0
- package/dist/cli-auth.d.ts +27 -1
- package/dist/cli-auth.js +56 -5
- package/dist/cli-config-command.d.ts +83 -1
- package/dist/cli-config-command.js +72 -10
- package/dist/cli-config.d.ts +8 -1
- package/dist/cli-config.js +20 -2
- package/dist/cli-options.d.ts +3 -1
- package/dist/cli-options.js +10 -1
- package/dist/history-response.d.ts +27 -4
- package/dist/history-response.js +5 -3
- package/dist/index.js +10 -4
- package/dist/usage.js +13 -5
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -14,23 +14,30 @@ npm install -g @rankwrangler/cli
|
|
|
14
14
|
rw --version
|
|
15
15
|
rw changelog
|
|
16
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
|
|
20
23
|
RR_LICENSE_KEY=rrk_... rw license status
|
|
21
24
|
# `rankwrangler` is also supported as an alias
|
|
22
25
|
```
|
|
23
26
|
|
|
24
|
-
`rw auth set
|
|
27
|
+
`rw auth set [licenseKey]` stores the license key in the platform secure store
|
|
25
28
|
(`macOS Keychain` on macOS). `rw auth status` reports whether the CLI will use an env override,
|
|
26
|
-
stored auth, or no auth. `rw auth
|
|
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.
|
|
27
31
|
|
|
28
32
|
`rw config set storage-dir <path>` saves the active storage directory globally. After that, every
|
|
29
33
|
CLI command reads and writes its non-secret config/data from that directory, while preserving
|
|
30
|
-
existing config values when switching. `
|
|
31
|
-
`
|
|
32
|
-
`
|
|
33
|
-
|
|
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.
|
|
34
41
|
|
|
35
42
|
## Development
|
|
36
43
|
|
|
@@ -60,9 +67,9 @@ matching `@rankwrangler/http-client` version is already available on npm.
|
|
|
60
67
|
|
|
61
68
|
```bash
|
|
62
69
|
cd packages/cli
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
set +a
|
|
66
|
-
npm whoami --userconfig ../../.npmrc
|
|
67
|
-
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
|
|
68
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,24 @@
|
|
|
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
|
+
|
|
5
23
|
## v0.5.0 - 2026-03-18
|
|
6
24
|
|
|
7
25
|
### Added
|
package/dist/cli-auth.d.ts
CHANGED
|
@@ -6,13 +6,16 @@ type AuthCommand = {
|
|
|
6
6
|
verb: string;
|
|
7
7
|
args: string[];
|
|
8
8
|
};
|
|
9
|
+
type AuthCommandOptions = {
|
|
10
|
+
stdin?: boolean;
|
|
11
|
+
};
|
|
9
12
|
export declare class CliAuthError extends Error {
|
|
10
13
|
readonly code: string;
|
|
11
14
|
readonly details?: unknown;
|
|
12
15
|
constructor(code: string, message: string, details?: unknown);
|
|
13
16
|
}
|
|
14
17
|
export declare const resolveApiKey: () => Promise<string | undefined>;
|
|
15
|
-
export declare const runAuthCommand: (command: AuthCommand, fail: CliFail) => Promise<{
|
|
18
|
+
export declare const runAuthCommand: (command: AuthCommand, fail: CliFail, options?: AuthCommandOptions) => Promise<{
|
|
16
19
|
source: string;
|
|
17
20
|
envOverride: boolean;
|
|
18
21
|
secureStore: {
|
|
@@ -83,4 +86,27 @@ export declare const runAuthCommand: (command: AuthCommand, fail: CliFail) => Pr
|
|
|
83
86
|
error: string;
|
|
84
87
|
};
|
|
85
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
|
+
}>;
|
|
86
112
|
export {};
|
package/dist/cli-auth.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
1
2
|
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
4
|
+
import readline from 'node:readline';
|
|
3
5
|
export const LICENSE_KEY_ENV_VAR = 'RR_LICENSE_KEY';
|
|
4
6
|
export const AUTH_SERVICE_NAME = 'RankWrangler CLI';
|
|
5
7
|
export const AUTH_ACCOUNT_NAME = 'license-key';
|
|
@@ -22,12 +24,12 @@ export const resolveApiKey = async () => {
|
|
|
22
24
|
const secureStore = await createSecureStore();
|
|
23
25
|
return (await secureStore.get()) ?? undefined;
|
|
24
26
|
};
|
|
25
|
-
export const runAuthCommand = async (command, fail) => {
|
|
27
|
+
export const runAuthCommand = async (command, fail, options = {}) => {
|
|
26
28
|
if (command.verb === 'status') {
|
|
27
29
|
return buildAuthStatus();
|
|
28
30
|
}
|
|
29
31
|
if (command.verb === 'set') {
|
|
30
|
-
const value = resolveAuthSetValue(command.args, fail);
|
|
32
|
+
const value = await resolveAuthSetValue(command.args, fail, options);
|
|
31
33
|
const secureStore = await createSecureStore();
|
|
32
34
|
await secureStore.set(value);
|
|
33
35
|
return {
|
|
@@ -45,7 +47,7 @@ export const runAuthCommand = async (command, fail) => {
|
|
|
45
47
|
}
|
|
46
48
|
fail('UNKNOWN_COMMAND', 'Unknown auth command', { verb: command.verb });
|
|
47
49
|
};
|
|
48
|
-
const buildAuthStatus = async () => {
|
|
50
|
+
export const buildAuthStatus = async () => {
|
|
49
51
|
const envLicenseKey = resolveEnvLicenseKey();
|
|
50
52
|
try {
|
|
51
53
|
const secureStore = await createSecureStore();
|
|
@@ -77,16 +79,65 @@ const buildAuthStatus = async () => {
|
|
|
77
79
|
};
|
|
78
80
|
}
|
|
79
81
|
};
|
|
80
|
-
const resolveAuthSetValue = (args, fail) => {
|
|
82
|
+
const resolveAuthSetValue = async (args, fail, options) => {
|
|
81
83
|
const positionalValue = args.join(' ').trim();
|
|
82
84
|
if (positionalValue) {
|
|
83
85
|
return positionalValue;
|
|
84
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
|
+
}
|
|
85
94
|
const envLicenseKey = resolveEnvLicenseKey();
|
|
86
95
|
if (envLicenseKey) {
|
|
87
96
|
return envLicenseKey;
|
|
88
97
|
}
|
|
89
|
-
|
|
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
|
+
});
|
|
90
141
|
};
|
|
91
142
|
const resolveEnvLicenseKey = () => {
|
|
92
143
|
const value = process.env[LICENSE_KEY_ENV_VAR]?.trim();
|
|
@@ -9,11 +9,93 @@ export declare const runConfigCommand: (command: ConfigCommand, config: CliConfi
|
|
|
9
9
|
path: string;
|
|
10
10
|
globalPath: string;
|
|
11
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
|
+
};
|
|
12
35
|
} | {
|
|
13
|
-
|
|
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;
|
|
14
71
|
storageDir: string;
|
|
15
72
|
path: string;
|
|
16
73
|
globalPath: string;
|
|
17
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;
|
|
18
100
|
}>;
|
|
19
101
|
export {};
|
|
@@ -1,14 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildAuthStatus } from './cli-auth.js';
|
|
2
|
+
import { resetConfig, saveConfig, switchStorageDir, unsetStorageDir, } from './cli-config.js';
|
|
2
3
|
import { normalizeBaseUrl } from './cli-options.js';
|
|
4
|
+
const CONFIG_KEYS = ['base-url', 'marketplace', 'storage-dir'];
|
|
3
5
|
export const runConfigCommand = async (command, config, paths, fail) => {
|
|
4
6
|
if (command.verb === 'show') {
|
|
5
7
|
return buildConfigResponse(paths, config);
|
|
6
8
|
}
|
|
7
|
-
if (command.verb === '
|
|
8
|
-
|
|
9
|
+
if (command.verb === 'get') {
|
|
10
|
+
const key = requireConfigKey(command.args[0], 'config get', fail);
|
|
9
11
|
return {
|
|
10
|
-
|
|
11
|
-
|
|
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,
|
|
12
29
|
};
|
|
13
30
|
}
|
|
14
31
|
if (command.verb !== 'set') {
|
|
@@ -38,19 +55,64 @@ export const runConfigCommand = async (command, config, paths, fail) => {
|
|
|
38
55
|
nextConfig.marketplaceId = value;
|
|
39
56
|
}
|
|
40
57
|
else {
|
|
41
|
-
|
|
42
|
-
key,
|
|
43
|
-
supportedKeys: ['base-url', 'marketplace', 'storage-dir'],
|
|
44
|
-
});
|
|
58
|
+
failUnsupportedConfigKey(key, fail);
|
|
45
59
|
}
|
|
46
60
|
await saveConfig(paths, nextConfig);
|
|
47
61
|
return buildConfigResponse(paths, nextConfig);
|
|
48
62
|
};
|
|
49
|
-
const buildConfigResponse = (paths, config) => {
|
|
63
|
+
const buildConfigResponse = async (paths, config) => {
|
|
50
64
|
return {
|
|
51
65
|
storageDir: paths.storageDir,
|
|
52
66
|
path: paths.configPath,
|
|
53
67
|
globalPath: paths.globalConfigPath,
|
|
54
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,
|
|
55
111
|
};
|
|
56
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,6 +18,7 @@ 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;
|
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;
|
|
@@ -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
|
@@ -5,7 +5,7 @@ import { resolveApiKey, runAuthCommand } from './cli-auth.js';
|
|
|
5
5
|
import { printBundledChangelog, printCliVersion } from './cli-metadata.js';
|
|
6
6
|
import { runConfigCommand } from './cli-config-command.js';
|
|
7
7
|
import { loadCliContext, loadCliPathsOrDefault } from './cli-config.js';
|
|
8
|
-
import { parseIntegerOption, requireAsins, requireMarketplaceId, requireSingleAsin, resolveBaseUrl, resolveHistoryMetrics, resolveHistoryWindow, } from './cli-options.js';
|
|
8
|
+
import { parseIntegerOption, requireAsins, requireMarketplaceId, requireSingleAsin, resolveBaseUrl, resolveHistoryBucket, resolveHistoryMetrics, resolveHistoryWindow, } from './cli-options.js';
|
|
9
9
|
import { fail, printSuccess, resolveError } from './cli-output.js';
|
|
10
10
|
import { buildCliHistoryResponse } from './history-response.js';
|
|
11
11
|
import { printUsage } from './usage.js';
|
|
@@ -18,7 +18,9 @@ const SUPPORTED_COMMANDS = new Set([
|
|
|
18
18
|
'auth:set',
|
|
19
19
|
'auth:clear',
|
|
20
20
|
'config:show',
|
|
21
|
-
'config:
|
|
21
|
+
'config:get',
|
|
22
|
+
'config:unset',
|
|
23
|
+
'config:reset',
|
|
22
24
|
'config:set',
|
|
23
25
|
]);
|
|
24
26
|
const { positionals, values } = parseArgs({
|
|
@@ -34,6 +36,8 @@ const { positionals, values } = parseArgs({
|
|
|
34
36
|
endAt: { type: 'string' },
|
|
35
37
|
days: { type: 'string' },
|
|
36
38
|
limit: { type: 'string' },
|
|
39
|
+
bucket: { type: 'string' },
|
|
40
|
+
stdin: { type: 'boolean' },
|
|
37
41
|
},
|
|
38
42
|
allowPositionals: true,
|
|
39
43
|
});
|
|
@@ -66,12 +70,12 @@ const main = async () => {
|
|
|
66
70
|
return;
|
|
67
71
|
}
|
|
68
72
|
if (command.resource === 'auth') {
|
|
69
|
-
printSuccess(await runAuthCommand(command, fail));
|
|
73
|
+
printSuccess(await runAuthCommand(command, fail, { stdin: Boolean(values.stdin) }));
|
|
70
74
|
return;
|
|
71
75
|
}
|
|
72
76
|
const apiKey = await resolveApiKey();
|
|
73
77
|
if (!apiKey) {
|
|
74
|
-
fail('MISSING_CONFIG', 'license key is required. run `rw auth set
|
|
78
|
+
fail('MISSING_CONFIG', 'license key is required. run `rw auth set`, `rw auth set --stdin`, or set RR_LICENSE_KEY');
|
|
75
79
|
}
|
|
76
80
|
const client = createRankWranglerClient({
|
|
77
81
|
baseUrl: resolveBaseUrl(optionValues, config, DEFAULT_API_BASE_URL, fail),
|
|
@@ -107,6 +111,7 @@ const runProductHistoryCommand = async (command, client, config) => {
|
|
|
107
111
|
const asin = requireSingleAsin(command.args, optionValues, fail);
|
|
108
112
|
const marketplaceId = requireMarketplaceId(optionValues, config);
|
|
109
113
|
const metrics = resolveHistoryMetrics(optionValues, fail);
|
|
114
|
+
const bucket = resolveHistoryBucket(optionValues, fail);
|
|
110
115
|
const historyWindow = resolveHistoryWindow(optionValues, fail);
|
|
111
116
|
const limit = parseIntegerOption({
|
|
112
117
|
value: optionValues.limit,
|
|
@@ -120,6 +125,7 @@ const runProductHistoryCommand = async (command, client, config) => {
|
|
|
120
125
|
asin,
|
|
121
126
|
metrics,
|
|
122
127
|
format: 'agent',
|
|
128
|
+
bucket,
|
|
123
129
|
limit,
|
|
124
130
|
...historyWindow,
|
|
125
131
|
});
|
package/dist/usage.js
CHANGED
|
@@ -20,13 +20,16 @@ export const printUsage = (paths) => {
|
|
|
20
20
|
' license status',
|
|
21
21
|
' license validate',
|
|
22
22
|
' auth status',
|
|
23
|
-
' auth set
|
|
23
|
+
' auth set [licenseKey]',
|
|
24
|
+
' auth set --stdin',
|
|
24
25
|
' auth clear',
|
|
25
26
|
' config show',
|
|
26
|
-
' config
|
|
27
|
+
' config get <key>',
|
|
27
28
|
' config set base-url <origin>',
|
|
28
29
|
' config set marketplace <marketplaceId>',
|
|
29
30
|
' config set storage-dir <path>',
|
|
31
|
+
' config unset <key>',
|
|
32
|
+
' config reset',
|
|
30
33
|
'',
|
|
31
34
|
'OPTIONS',
|
|
32
35
|
' -h, --help Show this help message',
|
|
@@ -35,10 +38,12 @@ export const printUsage = (paths) => {
|
|
|
35
38
|
` -m, --marketplace <id> Override marketplace (default: ${DEFAULT_MARKETPLACE_ID})`,
|
|
36
39
|
' --asin <ASIN> Add ASIN (repeatable)',
|
|
37
40
|
' --metrics <list> History metrics: bsr,price (default: bsr,price)',
|
|
41
|
+
' --bucket <unit> History buckets: auto,day,week,month (default: auto)',
|
|
38
42
|
' --days <N> History lookback window (30-3650, default: 365)',
|
|
39
43
|
' --startAt <ISO> History range start (ISO date/time)',
|
|
40
44
|
' --endAt <ISO> History range end (ISO date/time)',
|
|
41
|
-
' --limit <N>
|
|
45
|
+
' --limit <N> Internal point cap (1-10000, default: 5000)',
|
|
46
|
+
' --stdin Read auth secret from standard input',
|
|
42
47
|
'',
|
|
43
48
|
'FILES',
|
|
44
49
|
` ${paths.configPath} Active CLI config`,
|
|
@@ -55,13 +60,16 @@ export const printUsage = (paths) => {
|
|
|
55
60
|
'',
|
|
56
61
|
'EXAMPLES',
|
|
57
62
|
' rw auth set rrk_...',
|
|
63
|
+
' printf %s "$RR_LICENSE_KEY" | rw auth set --stdin',
|
|
58
64
|
` ${LICENSE_KEY_ENV_VAR}=rrk_... rw products get B0DV53VS61`,
|
|
59
65
|
' RR_STORAGE_DIR=~/.config/rankwrangler rw config show',
|
|
60
66
|
' rw --version',
|
|
61
67
|
' rw changelog',
|
|
68
|
+
' rw config get marketplace',
|
|
62
69
|
' rw config set storage-dir ~/.config/rankwrangler',
|
|
63
|
-
' rw
|
|
64
|
-
' 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',
|
|
65
73
|
' rankwrangler license status',
|
|
66
74
|
];
|
|
67
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,15 +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
|
-
"keytar": "
|
|
27
|
+
"@rankwrangler/http-client": "^0.6.0",
|
|
28
|
+
"keytar": "7.9.0"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
|
-
"@types/node": "
|
|
31
|
-
"typescript": "
|
|
31
|
+
"@types/node": "24.10.12",
|
|
32
|
+
"typescript": "5.9.3"
|
|
32
33
|
},
|
|
33
34
|
"packageManager": "bun@1.1.27"
|
|
34
35
|
}
|