@rankwrangler/cli 0.4.0 → 0.5.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 CHANGED
@@ -11,17 +11,26 @@ npm install -g @rankwrangler/cli
11
11
  ## Usage
12
12
 
13
13
  ```bash
14
- export RR_LICENSE_KEY=rrk_...
14
+ rw --version
15
+ rw changelog
16
+ rw auth set rrk_...
15
17
  rw config set storage-dir ~/.config/rankwrangler
16
18
  rw products get B0DV53VS61
17
19
  rw products history B0DV53VS61 --metrics bsr,price
20
+ RR_LICENSE_KEY=rrk_... rw license status
18
21
  # `rankwrangler` is also supported as an alias
19
22
  ```
20
23
 
21
- `RR_LICENSE_KEY` is the API-key source. `rw config set storage-dir <path>` saves the
22
- active storage directory globally. After that, every CLI command reads and writes its non-secret
23
- config/data from that directory, while preserving existing config values when switching.
24
- `RR_STORAGE_DIR`, `RR_API_URL`, and `RR_MARKETPLACE_ID` override saved CLI config when set.
24
+ `rw auth set <licenseKey>` stores the license key in the platform secure store
25
+ (`macOS Keychain` on macOS). `rw auth status` reports whether the CLI will use an env override,
26
+ stored auth, or no auth. `rw auth clear` removes the stored key.
27
+
28
+ `rw config set storage-dir <path>` saves the active storage directory globally. After that, every
29
+ CLI command reads and writes its non-secret config/data from that directory, while preserving
30
+ existing config values when switching. `RR_LICENSE_KEY`, `RR_STORAGE_DIR`, `RR_API_URL`, and
31
+ `RR_MARKETPLACE_ID` override saved CLI state when set for CI, automation, or agent runtimes.
32
+ `rw --version` prints the installed CLI version. `rw changelog` prints the latest bundled release
33
+ notes shipped with the package.
25
34
 
26
35
  ## Development
27
36
 
@@ -46,6 +55,9 @@ bun run release:check-cli-pack
46
55
 
47
56
  Then publish:
48
57
 
58
+ Publish `packages/http-client` first. `packages/cli` now fails fast on `npm publish` until the
59
+ matching `@rankwrangler/http-client` version is already available on npm.
60
+
49
61
  ```bash
50
62
  cd packages/cli
51
63
  set -a
@@ -0,0 +1,205 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## v0.5.0 - 2026-03-18
6
+
7
+ ### Added
8
+
9
+ - Add CLI secure-store auth with `rw auth set`, `rw auth status`, and `rw auth clear`, storing
10
+ license keys in the platform secure store while keeping `RR_LICENSE_KEY` as an override for
11
+ CI, automation, and agent runtimes.
12
+
13
+ ### Changed
14
+
15
+ - Change local development defaults so the app server no longer starts the job runner unless you
16
+ use the explicit jobs-enabled dev commands.
17
+
18
+ ### Fixed
19
+
20
+ - Fix top search terms scheduler timestamp handling so due datasets are selected and refreshed
21
+ reliably.
22
+
23
+ ## v0.4.1 - 2026-03-09
24
+
25
+ ### Added
26
+
27
+ - Add CLI `rw --version` and `rw changelog` commands, including bundled release notes in the
28
+ published npm package.
29
+
30
+ ### Changed
31
+
32
+ - Rotate API key generation by email so creating a new key for the same email replaces older
33
+ active keys.
34
+ - Remove the remaining PostHog integration from the server runtime, config, and related docs.
35
+
36
+ ### Fixed
37
+
38
+ - Make CLI publish fail fast until the matching `@rankwrangler/http-client` version is already on
39
+ npm, reducing release-order mistakes.
40
+
41
+ ## v0.4.0 - 2026-03-09
42
+
43
+ ### Added
44
+
45
+ - Add `RR_STORAGE_DIR` and document CLI env vars in `.env.example` so local CLI testing can use
46
+ env-based configuration end to end.
47
+
48
+ ### Changed
49
+
50
+ - Make CLI environment variables override saved config for storage-dir, API origin, and
51
+ marketplace values.
52
+
53
+ ### Fixed
54
+
55
+ - Fix the built CLI package to run under Node after the CLI module split and strengthen release
56
+ pack validation around the built artifact.
57
+
58
+ ## v0.3.0 - 2026-03-09
59
+
60
+ ### Added
61
+
62
+ - Add BA Top Search Terms ingestion, trend views, keyword search, and job execution visibility
63
+ across the dashboard.
64
+ - Add product facet classification, facet metrics, and facet badges to improve dashboard filtering
65
+ and product detail context.
66
+ - Add persistent CLI storage-directory support so local CLI config/data can move to a custom path.
67
+ - Add `RR_STORAGE_DIR` so CLI storage location can be controlled by environment as well as saved
68
+ config.
69
+
70
+ ### Changed
71
+
72
+ - Redesign the search terms dashboard UI with richer toolbar controls, stat sparklines, clearer
73
+ metric timing, and stronger layout polish.
74
+ - Improve top search terms scheduling, batching, report polling, and status performance so refresh
75
+ throughput and visibility stay stable as datasets grow.
76
+ - Centralize adaptive SP-API limiter tuning and expose limiter stats for better operational control.
77
+ - Breaking: require `RR_LICENSE_KEY` for CLI auth and stop storing API keys in CLI config.
78
+
79
+ ### Fixed
80
+
81
+ - Fix weekly top search terms windows to align to Sunday-Saturday boundaries and correct
82
+ reclassification batch payload handling.
83
+ - Tighten merch keyword filtering and category-slot parsing to reduce seasonal/accessory noise
84
+ without dropping valid terms.
85
+ - Fix Node runtime imports in the built CLI package and strengthen release pack validation after
86
+ the CLI module split.
87
+ - Fix facet metrics timestamp anchoring, SQL casting, and drawer badge data flow in dashboard
88
+ surfaces.
89
+
90
+ ## v0.2.3 - 2026-03-01
91
+
92
+ ### Added
93
+
94
+ - Add CLI `products history` agent output for BSR and price metric series.
95
+ - Add dashboard logs page and event log pipeline with Keepa queue/poll visibility.
96
+ - Add tiered Keepa scheduling with improved enqueue metrics and status reporting.
97
+ - Add shared history chart usage across website and extension surfaces.
98
+ - Add extension UI preview workflow updates with safer placeholder license key handling.
99
+
100
+ ### Changed
101
+
102
+ - Handle SP-API no-payload catalog responses as product deletions in sync flows.
103
+ - Refine event logging behavior, server documentation, and Keepa policy labeling.
104
+
105
+ ### Fixed
106
+
107
+ - Fix product drawer chart skeleton height and sponsored carousel stats placement.
108
+ - Fix Caddy build wiring to include the history-chart workspace dependency.
109
+ - Improve dashboard log view layout behavior and readability.
110
+
111
+ ## v0.2.2 - 2026-02-24
112
+
113
+ ### Added
114
+
115
+ - Add dashboard-style BSR chart styling to the extension product view.
116
+ - Add Keepa stale sync tests and documentation coverage.
117
+
118
+ ### Changed
119
+
120
+ - Add bottom border to the appearance settings section for stronger visual separation.
121
+ - Clarify agent guidance for work-in-progress rewrite expectations.
122
+
123
+ ### Fixed
124
+
125
+ - Clarify history chart date labeling behavior.
126
+
127
+ ## v0.2.1 - 2026-02-22
128
+
129
+ ### Added
130
+
131
+ - Add fuzzy dashboard product search and stronger filter-driven sidebar workflows.
132
+ - Add extension BSR history graph flow, including history sheet deep-linking from BSR badges.
133
+ - Add admin job runtime observability, queue log browsing, and per-source drilldown stats.
134
+ - Add Keepa sync loading states and richer history date-range controls.
135
+
136
+ ### Changed
137
+
138
+ - Move product ingest processing to an event-driven queue flow and align Keepa refresh to one-shot
139
+ enqueue jobs.
140
+ - Rename ingest queue labeling to the SP-API sync queue across admin surfaces.
141
+ - Refine BSR/history chart interactions, sparse-range handling, and loading/empty-state behavior.
142
+ - Improve extension build prebuild dependency handling and dashboard/admin UI polish.
143
+ - Clarify docs for Keepa automatic vs manual behavior.
144
+
145
+ ## v0.2.0 - 2026-02-20
146
+
147
+ ### Added
148
+
149
+ - Add Keepa history import flow and dashboard history UI.
150
+ - Add Keepa refresh queue processing and Keepa status metrics in the dashboard.
151
+ - Add admin job runtime observability endpoints and dashboard visibility.
152
+ - Add extension BSR history modal with on-demand history loading flow.
153
+ - Add public + app product history query/load procedures for Keepa-backed BSR history.
154
+ - Add public API usage consumption to history query/load endpoints to enforce API key limits.
155
+
156
+ ### Changed
157
+
158
+ - Align Keepa refresh behavior to a one-shot enqueue model.
159
+ - Redesign dashboard sidebar header/layout, Keepa history panel, filters, and settings modal UX.
160
+ - Standardize external naming to `API key` across public API routes, CLI commands, extension UI
161
+ copy, and related docs.
162
+ - Rename CLI auth commands from `license status|validate` to `api-key status|validate`.
163
+ - Rename public API auth namespace from `api.public.license.*` to `api.public.apiKey.*` and app
164
+ namespace from `api.app.license.*` to `api.app.apiKey.*`.
165
+ - Switch CLI environment fallback from `RR_LICENSE_KEY` to `RR_API_KEY`.
166
+
167
+ ### Fixed
168
+
169
+ - Fix Keepa fetch worker job payload handling.
170
+
171
+ ## v0.1.3 - 2026-02-17
172
+
173
+ ### Changed
174
+
175
+ - Set `@rankwrangler/cli` to expose both `rw` (primary) and `rankwrangler` binaries.
176
+ - Tighten CLI help output into a concise man-style format and remove principles text from runtime
177
+ help.
178
+ - Remove legacy CLI command and flag aliases by default (`get-product-info`,
179
+ `get-product-info-batch`, `products get-batch`, `--licenseKey`, `--url`, `--marketplaceId`,
180
+ `--asins`).
181
+ - Add explicit CLI policy guidance that legacy aliases/compatibility shims require explicit user
182
+ direction.
183
+
184
+ ## v0.1.2 - 2026-02-17
185
+
186
+ ### Changed
187
+
188
+ - Update extension's pinned HTTP client tarball dependency to
189
+ `rankwrangler-http-client-0.1.2.tgz`.
190
+ - Update release docs to require synchronized versions between `CHANGELOG.md` releases and npm
191
+ packages.
192
+
193
+ ## v0.1.0 - 2026-02-16
194
+
195
+ ### Added
196
+
197
+ - Add public batch product endpoint (`api.public.getProductInfoBatch`) to support multi-ASIN workflows.
198
+ - Add CLI multi-ASIN product lookup (`products get <asin...>`) with default marketplace config and per-command override (`--marketplace` / `-m`).
199
+ - Add public license usage endpoints used by CLI/public HTTP clients for status and consume flows.
200
+
201
+ ### Changed
202
+
203
+ - Rename and standardize the typed npm client package to `@merchbase/rankwrangler-http-client`.
204
+ - Set initial HTTP client package release to `0.1.0` in `packages/http-client/package.json`.
205
+ - Document npm publish and versioning policy for the typed HTTP client in `docs/http-client-spec.md`.
@@ -0,0 +1,86 @@
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
+ export declare class CliAuthError extends Error {
10
+ readonly code: string;
11
+ readonly details?: unknown;
12
+ constructor(code: string, message: string, details?: unknown);
13
+ }
14
+ export declare const resolveApiKey: () => Promise<string | undefined>;
15
+ export declare const runAuthCommand: (command: AuthCommand, fail: CliFail) => Promise<{
16
+ source: string;
17
+ envOverride: boolean;
18
+ secureStore: {
19
+ backend: string;
20
+ serviceName: string;
21
+ accountName: string;
22
+ available: boolean;
23
+ hasStoredLicenseKey: boolean;
24
+ error?: undefined;
25
+ };
26
+ } | {
27
+ source: string;
28
+ envOverride: boolean;
29
+ secureStore: {
30
+ backend: string;
31
+ serviceName: string;
32
+ accountName: string;
33
+ available: boolean;
34
+ hasStoredLicenseKey: null;
35
+ error: string;
36
+ };
37
+ } | {
38
+ saved: boolean;
39
+ source: string;
40
+ envOverride: boolean;
41
+ secureStore: {
42
+ backend: string;
43
+ serviceName: string;
44
+ accountName: string;
45
+ available: boolean;
46
+ hasStoredLicenseKey: boolean;
47
+ error?: undefined;
48
+ };
49
+ } | {
50
+ saved: boolean;
51
+ source: string;
52
+ envOverride: boolean;
53
+ secureStore: {
54
+ backend: string;
55
+ serviceName: string;
56
+ accountName: string;
57
+ available: boolean;
58
+ hasStoredLicenseKey: null;
59
+ error: string;
60
+ };
61
+ } | {
62
+ cleared: boolean;
63
+ source: string;
64
+ envOverride: boolean;
65
+ secureStore: {
66
+ backend: string;
67
+ serviceName: string;
68
+ accountName: string;
69
+ available: boolean;
70
+ hasStoredLicenseKey: boolean;
71
+ error?: undefined;
72
+ };
73
+ } | {
74
+ cleared: boolean;
75
+ source: string;
76
+ envOverride: boolean;
77
+ secureStore: {
78
+ backend: string;
79
+ serviceName: string;
80
+ accountName: string;
81
+ available: boolean;
82
+ hasStoredLicenseKey: null;
83
+ error: string;
84
+ };
85
+ }>;
86
+ export {};
@@ -0,0 +1,171 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ export const LICENSE_KEY_ENV_VAR = 'RR_LICENSE_KEY';
4
+ export const AUTH_SERVICE_NAME = 'RankWrangler CLI';
5
+ export const AUTH_ACCOUNT_NAME = 'license-key';
6
+ const INTERNAL_SECRET_STORE_DIR_ENV_VAR = 'RW_INTERNAL_CLI_SECRET_STORE_DIR';
7
+ export class CliAuthError extends Error {
8
+ code;
9
+ details;
10
+ constructor(code, message, details) {
11
+ super(message);
12
+ this.name = 'CliAuthError';
13
+ this.code = code;
14
+ this.details = details;
15
+ }
16
+ }
17
+ export const resolveApiKey = async () => {
18
+ const envLicenseKey = resolveEnvLicenseKey();
19
+ if (envLicenseKey) {
20
+ return envLicenseKey;
21
+ }
22
+ const secureStore = await createSecureStore();
23
+ return (await secureStore.get()) ?? undefined;
24
+ };
25
+ export const runAuthCommand = async (command, fail) => {
26
+ if (command.verb === 'status') {
27
+ return buildAuthStatus();
28
+ }
29
+ if (command.verb === 'set') {
30
+ const value = resolveAuthSetValue(command.args, fail);
31
+ const secureStore = await createSecureStore();
32
+ await secureStore.set(value);
33
+ return {
34
+ ...(await buildAuthStatus()),
35
+ saved: true,
36
+ };
37
+ }
38
+ if (command.verb === 'clear') {
39
+ const secureStore = await createSecureStore();
40
+ const cleared = await secureStore.clear();
41
+ return {
42
+ ...(await buildAuthStatus()),
43
+ cleared,
44
+ };
45
+ }
46
+ fail('UNKNOWN_COMMAND', 'Unknown auth command', { verb: command.verb });
47
+ };
48
+ const buildAuthStatus = async () => {
49
+ const envLicenseKey = resolveEnvLicenseKey();
50
+ try {
51
+ const secureStore = await createSecureStore();
52
+ const storedLicenseKey = await secureStore.get();
53
+ return {
54
+ source: envLicenseKey ? 'env' : storedLicenseKey ? 'secure-store' : 'none',
55
+ envOverride: Boolean(envLicenseKey),
56
+ secureStore: {
57
+ backend: secureStore.backend,
58
+ serviceName: secureStore.serviceName,
59
+ accountName: secureStore.accountName,
60
+ available: true,
61
+ hasStoredLicenseKey: Boolean(storedLicenseKey),
62
+ },
63
+ };
64
+ }
65
+ catch (error) {
66
+ return {
67
+ source: envLicenseKey ? 'env' : 'none',
68
+ envOverride: Boolean(envLicenseKey),
69
+ secureStore: {
70
+ backend: getSecureStoreBackendLabel(),
71
+ serviceName: AUTH_SERVICE_NAME,
72
+ accountName: AUTH_ACCOUNT_NAME,
73
+ available: false,
74
+ hasStoredLicenseKey: null,
75
+ error: error instanceof Error ? error.message : 'Unknown secure-store error',
76
+ },
77
+ };
78
+ }
79
+ };
80
+ const resolveAuthSetValue = (args, fail) => {
81
+ const positionalValue = args.join(' ').trim();
82
+ if (positionalValue) {
83
+ return positionalValue;
84
+ }
85
+ const envLicenseKey = resolveEnvLicenseKey();
86
+ if (envLicenseKey) {
87
+ return envLicenseKey;
88
+ }
89
+ fail('INVALID_INPUT', `auth set requires <licenseKey> or ${LICENSE_KEY_ENV_VAR}`);
90
+ };
91
+ const resolveEnvLicenseKey = () => {
92
+ const value = process.env[LICENSE_KEY_ENV_VAR]?.trim();
93
+ return value ? value : undefined;
94
+ };
95
+ const createSecureStore = async () => {
96
+ const internalSecretStoreDir = process.env[INTERNAL_SECRET_STORE_DIR_ENV_VAR]?.trim();
97
+ if (internalSecretStoreDir) {
98
+ return createFileSecretStore(path.resolve(internalSecretStoreDir));
99
+ }
100
+ const keytar = await loadKeytar();
101
+ return {
102
+ backend: getSecureStoreBackendLabel(),
103
+ serviceName: AUTH_SERVICE_NAME,
104
+ accountName: AUTH_ACCOUNT_NAME,
105
+ get: () => keytar.getPassword(AUTH_SERVICE_NAME, AUTH_ACCOUNT_NAME),
106
+ set: value => keytar.setPassword(AUTH_SERVICE_NAME, AUTH_ACCOUNT_NAME, value),
107
+ clear: () => keytar.deletePassword(AUTH_SERVICE_NAME, AUTH_ACCOUNT_NAME),
108
+ };
109
+ };
110
+ const loadKeytar = async () => {
111
+ try {
112
+ const imported = (await import('keytar'));
113
+ return imported.default ?? imported;
114
+ }
115
+ catch (error) {
116
+ throw new CliAuthError('SECURE_STORE_UNAVAILABLE', 'platform secure store is unavailable', error instanceof Error ? { cause: error.message } : undefined);
117
+ }
118
+ };
119
+ const getSecureStoreBackendLabel = () => {
120
+ switch (process.platform) {
121
+ case 'darwin':
122
+ return 'macos-keychain';
123
+ case 'win32':
124
+ return 'windows-credential-manager';
125
+ default:
126
+ return 'platform-secure-store';
127
+ }
128
+ };
129
+ const createFileSecretStore = (secretStoreDir) => {
130
+ const secretPath = path.join(secretStoreDir, 'license-key.json');
131
+ return {
132
+ backend: 'test-file-store',
133
+ serviceName: AUTH_SERVICE_NAME,
134
+ accountName: AUTH_ACCOUNT_NAME,
135
+ get: async () => {
136
+ try {
137
+ const raw = await readFile(secretPath, 'utf8');
138
+ const parsed = JSON.parse(raw);
139
+ return typeof parsed.licenseKey === 'string' ? parsed.licenseKey : null;
140
+ }
141
+ catch (error) {
142
+ if (error &&
143
+ typeof error === 'object' &&
144
+ 'code' in error &&
145
+ error.code === 'ENOENT') {
146
+ return null;
147
+ }
148
+ throw new CliAuthError('INVALID_CONFIG', 'failed to read secure-store test data');
149
+ }
150
+ },
151
+ set: async (value) => {
152
+ await mkdir(secretStoreDir, { recursive: true });
153
+ await writeFile(secretPath, `${JSON.stringify({ licenseKey: value }, null, 2)}\n`, 'utf8');
154
+ },
155
+ clear: async () => {
156
+ try {
157
+ await rm(secretPath);
158
+ return true;
159
+ }
160
+ catch (error) {
161
+ if (error &&
162
+ typeof error === 'object' &&
163
+ 'code' in error &&
164
+ error.code === 'ENOENT') {
165
+ return false;
166
+ }
167
+ throw error;
168
+ }
169
+ },
170
+ };
171
+ };
@@ -0,0 +1,19 @@
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
+ } | {
13
+ cleared: boolean;
14
+ storageDir: string;
15
+ path: string;
16
+ globalPath: string;
17
+ config: CliConfig;
18
+ }>;
19
+ export {};
@@ -0,0 +1,56 @@
1
+ import { clearConfig, saveConfig, switchStorageDir } from './cli-config.js';
2
+ import { normalizeBaseUrl } from './cli-options.js';
3
+ export const runConfigCommand = async (command, config, paths, fail) => {
4
+ if (command.verb === 'show') {
5
+ return buildConfigResponse(paths, config);
6
+ }
7
+ if (command.verb === 'clear') {
8
+ await clearConfig(paths);
9
+ return {
10
+ ...buildConfigResponse(paths, {}),
11
+ cleared: true,
12
+ };
13
+ }
14
+ if (command.verb !== 'set') {
15
+ fail('UNKNOWN_COMMAND', 'Unknown config command', { verb: command.verb });
16
+ }
17
+ const [key, ...valueParts] = command.args;
18
+ if (!key || valueParts.length === 0) {
19
+ fail('INVALID_INPUT', 'config set requires <key> <value>');
20
+ }
21
+ const value = valueParts.join(' ').trim();
22
+ if (!value) {
23
+ fail('INVALID_INPUT', 'config set value cannot be empty');
24
+ }
25
+ if (key === 'storage-dir') {
26
+ const nextState = await switchStorageDir({
27
+ currentConfig: config,
28
+ currentPaths: paths,
29
+ requestedStorageDir: value,
30
+ });
31
+ return buildConfigResponse(nextState.paths, nextState.config);
32
+ }
33
+ const nextConfig = { ...config };
34
+ if (key === 'base-url') {
35
+ nextConfig.baseUrl = normalizeBaseUrl(value, fail);
36
+ }
37
+ else if (key === 'marketplace') {
38
+ nextConfig.marketplaceId = value;
39
+ }
40
+ else {
41
+ fail('INVALID_INPUT', 'unsupported config key', {
42
+ key,
43
+ supportedKeys: ['base-url', 'marketplace', 'storage-dir'],
44
+ });
45
+ }
46
+ await saveConfig(paths, nextConfig);
47
+ return buildConfigResponse(paths, nextConfig);
48
+ };
49
+ const buildConfigResponse = (paths, config) => {
50
+ return {
51
+ storageDir: paths.storageDir,
52
+ path: paths.configPath,
53
+ globalPath: paths.globalConfigPath,
54
+ config,
55
+ };
56
+ };
@@ -0,0 +1,2 @@
1
+ export declare const printCliVersion: () => void;
2
+ export declare const printBundledChangelog: () => void;
@@ -0,0 +1,31 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ const PACKAGE_JSON_PATH = fileURLToPath(new URL('../package.json', import.meta.url));
4
+ const CHANGELOG_PATH = fileURLToPath(new URL('./CHANGELOG.md', import.meta.url));
5
+ const RELEASE_HEADING_PATTERN = /^## v\d+\.\d+\.\d+ - \d{4}-\d{2}-\d{2}$/gm;
6
+ export const printCliVersion = () => {
7
+ const metadata = readPackageMetadata();
8
+ console.log(metadata.version);
9
+ };
10
+ export const printBundledChangelog = () => {
11
+ const changelog = readBundledChangelog();
12
+ const latestEntry = extractLatestReleaseEntry(changelog);
13
+ console.log(latestEntry);
14
+ };
15
+ const readPackageMetadata = () => {
16
+ return JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf8'));
17
+ };
18
+ const readBundledChangelog = () => {
19
+ return readFileSync(CHANGELOG_PATH, 'utf8').trim();
20
+ };
21
+ const extractLatestReleaseEntry = (changelog) => {
22
+ const matches = Array.from(changelog.matchAll(RELEASE_HEADING_PATTERN));
23
+ if (matches.length === 0) {
24
+ throw new Error('Bundled changelog is missing a release entry');
25
+ }
26
+ const latestMatch = matches[0];
27
+ const nextMatch = matches[1];
28
+ const start = latestMatch.index ?? 0;
29
+ const end = nextMatch?.index ?? changelog.length;
30
+ return changelog.slice(start, end).trim();
31
+ };
@@ -11,6 +11,7 @@ export type CliOptionValues = {
11
11
  days?: string;
12
12
  limit?: string;
13
13
  help?: boolean;
14
+ version?: boolean;
14
15
  };
15
16
  export declare const requireMarketplaceId: (values: CliOptionValues, config: CliConfig) => string;
16
17
  export declare const requireAsins: (commandArgs: string[], values: CliOptionValues, fail: CliFail) => string[];
@@ -21,7 +22,6 @@ export declare const resolveHistoryWindow: (values: CliOptionValues, fail: CliFa
21
22
  endAt?: string | undefined;
22
23
  startAt?: string | undefined;
23
24
  };
24
- export declare const resolveApiKey: () => string | undefined;
25
25
  export declare const resolveBaseUrl: (values: CliOptionValues, config: CliConfig, defaultBaseUrl: string, fail: CliFail) => string;
26
26
  export declare const normalizeBaseUrl: (value: string, fail: CliFail) => string;
27
27
  export declare const parseIntegerOption: ({ value, optionName, min, max, defaultValue, }: {
@@ -56,9 +56,6 @@ export const resolveHistoryWindow = (values, fail) => {
56
56
  }, fail),
57
57
  };
58
58
  };
59
- export const resolveApiKey = () => {
60
- return process.env.RR_LICENSE_KEY;
61
- };
62
59
  export const resolveBaseUrl = (values, config, defaultBaseUrl, fail) => {
63
60
  return normalizeBaseUrl(values.baseUrl ?? config.baseUrl ?? process.env.RR_API_URL ?? defaultBaseUrl, fail);
64
61
  };
@@ -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) {
package/dist/index.js CHANGED
@@ -1,8 +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 { clearConfig, loadCliContext, loadCliPathsOrDefault, saveConfig, switchStorageDir, } from './cli-config.js';
5
- import { normalizeBaseUrl, parseIntegerOption, requireAsins, requireMarketplaceId, requireSingleAsin, resolveApiKey, resolveBaseUrl, resolveHistoryMetrics, resolveHistoryWindow, } from './cli-options.js';
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
+ import { loadCliContext, loadCliPathsOrDefault } from './cli-config.js';
8
+ import { parseIntegerOption, requireAsins, requireMarketplaceId, requireSingleAsin, resolveBaseUrl, resolveHistoryMetrics, resolveHistoryWindow, } from './cli-options.js';
6
9
  import { fail, printSuccess, resolveError } from './cli-output.js';
7
10
  import { buildCliHistoryResponse } from './history-response.js';
8
11
  import { printUsage } from './usage.js';
@@ -11,6 +14,9 @@ const SUPPORTED_COMMANDS = new Set([
11
14
  'products:history',
12
15
  'license:status',
13
16
  'license:validate',
17
+ 'auth:status',
18
+ 'auth:set',
19
+ 'auth:clear',
14
20
  'config:show',
15
21
  'config:clear',
16
22
  'config:set',
@@ -19,6 +25,7 @@ const { positionals, values } = parseArgs({
19
25
  args: process.argv.slice(2),
20
26
  options: {
21
27
  help: { type: 'boolean', short: 'h' },
28
+ version: { type: 'boolean' },
22
29
  baseUrl: { type: 'string' },
23
30
  marketplace: { type: 'string', short: 'm' },
24
31
  asin: { type: 'string', multiple: true },
@@ -32,10 +39,21 @@ const { positionals, values } = parseArgs({
32
39
  });
33
40
  const main = async () => {
34
41
  const optionValues = values;
42
+ if (optionValues.version) {
43
+ printCliVersion();
44
+ return;
45
+ }
35
46
  if (values.help || positionals.length === 0) {
36
47
  printUsage(await loadCliPathsOrDefault());
37
48
  process.exit(values.help ? 0 : 1);
38
49
  }
50
+ const metaCommand = resolveMetaCommand(positionals);
51
+ if (metaCommand) {
52
+ if (metaCommand.name === 'changelog') {
53
+ printBundledChangelog();
54
+ return;
55
+ }
56
+ }
39
57
  const command = resolveCommandOrFail(positionals);
40
58
  if (!isSupportedCommand(command)) {
41
59
  fail('UNKNOWN_COMMAND', 'Unknown command', {
@@ -44,12 +62,16 @@ const main = async () => {
44
62
  }
45
63
  const { config, paths } = await loadCliContext();
46
64
  if (command.resource === 'config') {
47
- printSuccess(await runConfigCommand(command, config, paths));
65
+ printSuccess(await runConfigCommand(command, config, paths, fail));
66
+ return;
67
+ }
68
+ if (command.resource === 'auth') {
69
+ printSuccess(await runAuthCommand(command, fail));
48
70
  return;
49
71
  }
50
- const apiKey = resolveApiKey();
72
+ const apiKey = await resolveApiKey();
51
73
  if (!apiKey) {
52
- fail('MISSING_CONFIG', 'api key is required. set RR_LICENSE_KEY');
74
+ fail('MISSING_CONFIG', 'license key is required. run `rw auth set <licenseKey>` or set RR_LICENSE_KEY');
53
75
  }
54
76
  const client = createRankWranglerClient({
55
77
  baseUrl: resolveBaseUrl(optionValues, config, DEFAULT_API_BASE_URL, fail),
@@ -108,59 +130,11 @@ const runProductHistoryCommand = async (command, client, config) => {
108
130
  response: response,
109
131
  });
110
132
  };
111
- const runConfigCommand = async (command, config, paths) => {
112
- if (command.verb === 'show') {
113
- return buildConfigResponse(paths, config);
114
- }
115
- if (command.verb === 'clear') {
116
- await clearConfig(paths);
117
- return {
118
- ...buildConfigResponse(paths, {}),
119
- cleared: true,
120
- };
121
- }
122
- if (command.verb !== 'set') {
123
- fail('UNKNOWN_COMMAND', 'Unknown config command', { verb: command.verb });
124
- }
125
- const [key, ...valueParts] = command.args;
126
- if (!key || valueParts.length === 0) {
127
- fail('INVALID_INPUT', 'config set requires <key> <value>');
128
- }
129
- const value = valueParts.join(' ').trim();
130
- if (!value) {
131
- fail('INVALID_INPUT', 'config set value cannot be empty');
133
+ const resolveMetaCommand = (inputPositionals) => {
134
+ if (inputPositionals.length === 1 && inputPositionals[0] === 'changelog') {
135
+ return { name: 'changelog' };
132
136
  }
133
- if (key === 'storage-dir') {
134
- const nextState = await switchStorageDir({
135
- currentConfig: config,
136
- currentPaths: paths,
137
- requestedStorageDir: value,
138
- });
139
- return buildConfigResponse(nextState.paths, nextState.config);
140
- }
141
- const nextConfig = { ...config };
142
- if (key === 'base-url') {
143
- nextConfig.baseUrl = normalizeBaseUrl(value, fail);
144
- }
145
- else if (key === 'marketplace') {
146
- nextConfig.marketplaceId = value;
147
- }
148
- else {
149
- fail('INVALID_INPUT', 'unsupported config key', {
150
- key,
151
- supportedKeys: ['base-url', 'marketplace', 'storage-dir'],
152
- });
153
- }
154
- await saveConfig(paths, nextConfig);
155
- return buildConfigResponse(paths, nextConfig);
156
- };
157
- const buildConfigResponse = (paths, config) => {
158
- return {
159
- storageDir: paths.storageDir,
160
- path: paths.configPath,
161
- globalPath: paths.globalConfigPath,
162
- config,
163
- };
137
+ return null;
164
138
  };
165
139
  const resolveCommand = (inputPositionals) => {
166
140
  const [first, second, ...rest] = inputPositionals;
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 = [
@@ -5,14 +6,22 @@ export const printUsage = (paths) => {
5
6
  ' rw, rankwrangler - RankWrangler command line interface',
6
7
  '',
7
8
  'SYNOPSIS',
9
+ ' rw --version',
10
+ ' rankwrangler --version',
11
+ ' rw changelog',
12
+ ' rankwrangler changelog',
8
13
  ' rw <resource> <verb> [args...] [options]',
9
14
  ' rankwrangler <resource> <verb> [args...] [options]',
10
15
  '',
11
16
  'COMMANDS',
17
+ ' changelog',
12
18
  ' products get <ASIN...>',
13
19
  ' products history <ASIN>',
14
20
  ' license status',
15
21
  ' license validate',
22
+ ' auth status',
23
+ ' auth set <licenseKey>',
24
+ ' auth clear',
16
25
  ' config show',
17
26
  ' config clear',
18
27
  ' config set base-url <origin>',
@@ -21,6 +30,7 @@ export const printUsage = (paths) => {
21
30
  '',
22
31
  'OPTIONS',
23
32
  ' -h, --help Show this help message',
33
+ ' --version Print the CLI version',
24
34
  ' --baseUrl <origin> Override API origin',
25
35
  ` -m, --marketplace <id> Override marketplace (default: ${DEFAULT_MARKETPLACE_ID})`,
26
36
  ' --asin <ASIN> Add ASIN (repeatable)',
@@ -35,7 +45,7 @@ export const printUsage = (paths) => {
35
45
  ` ${paths.globalConfigPath} Global CLI settings`,
36
46
  '',
37
47
  'ENVIRONMENT',
38
- ' RR_LICENSE_KEY API key source',
48
+ ` ${LICENSE_KEY_ENV_VAR} License key override for CI/automation`,
39
49
  ' RR_API_URL API origin fallback',
40
50
  ' RR_MARKETPLACE_ID Marketplace fallback',
41
51
  ' RR_STORAGE_DIR Storage directory override',
@@ -44,8 +54,11 @@ export const printUsage = (paths) => {
44
54
  ' RR_HISTORY_METRICS Comma-separated history metrics fallback',
45
55
  '',
46
56
  'EXAMPLES',
47
- ' RR_LICENSE_KEY=rrk_... rw products get B0DV53VS61',
57
+ ' rw auth set rrk_...',
58
+ ` ${LICENSE_KEY_ENV_VAR}=rrk_... rw products get B0DV53VS61`,
48
59
  ' RR_STORAGE_DIR=~/.config/rankwrangler rw config show',
60
+ ' rw --version',
61
+ ' rw changelog',
49
62
  ' rw config set storage-dir ~/.config/rankwrangler',
50
63
  ' rw products history B0DV53VS61 --metrics bsr,price',
51
64
  ' rw products history B0DV53VS61 --startAt 2025-01-01 --endAt 2025-12-31',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rankwrangler/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Official RankWrangler command line interface",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,14 +14,17 @@
14
14
  "README.md"
15
15
  ],
16
16
  "scripts": {
17
- "build": "tsc -p tsconfig.json && node scripts/fix-dist-imports.mjs",
18
- "test:e2e": "bun test test/index.e2e.test.ts"
17
+ "build": "tsc -p tsconfig.json && node scripts/fix-dist-imports.mjs && node scripts/sync-changelog.mjs",
18
+ "prepack": "node scripts/sync-changelog.mjs",
19
+ "test:e2e": "bun test test/index.e2e.test.ts",
20
+ "prepublishOnly": "node ../../scripts/release/ensure-cli-http-client-published.mjs"
19
21
  },
20
22
  "publishConfig": {
21
23
  "access": "public"
22
24
  },
23
25
  "dependencies": {
24
- "@rankwrangler/http-client": "^0.4.0"
26
+ "@rankwrangler/http-client": "^0.5.0",
27
+ "keytar": "^7.9.0"
25
28
  },
26
29
  "devDependencies": {
27
30
  "@types/node": "^24.3.0",