@maixio/pstore 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-05-09
4
+
5
+ Initial public release of `@maixio/pstore`.
6
+
7
+ - Added PlayStation Store lookup by Concept ID and Product ID.
8
+ - Added lightweight search, category browse, and browse window commands.
9
+ - Added built-in catalog UUID registry with validation and live discovery helpers.
10
+ - Added multi-locale support for HK, TW, JP, US, GB, and CN storefronts.
11
+ - Added optional Batarang page extraction for publisher, descriptions, content rating, and localized title metadata.
12
+ - Added PlayStation Network status check command and SDK API.
13
+ - Added TypeScript SDK entrypoint and `pstore` CLI.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maixiang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,515 @@
1
+ # pstore
2
+
3
+ Unofficial PlayStation Store Web API client for querying publicly available storefront metadata. It ships as both a CLI and a TypeScript SDK.
4
+
5
+ > Disclaimer: pstore is a personal, unofficial project. It is not affiliated with, endorsed by, sponsored by, or authorized by Sony Interactive Entertainment, PlayStation, or any related company. PlayStation, PSN, and related marks are trademarks of their respective owners. The upstream PlayStation Store web APIs are not a public stable contract and may change without notice.
6
+
7
+ Chinese documentation is available in [README.zh-CN.md](./README.zh-CN.md).
8
+
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ # Lookup a game
13
+ pstore lookup 10014149
14
+
15
+ # Search the store
16
+ pstore search diablo
17
+
18
+ # Browse a category
19
+ pstore category pre-orders
20
+ ```
21
+
22
+ ## Installation
23
+
24
+ Run directly:
25
+
26
+ ```bash
27
+ bunx @maixio/pstore --help
28
+ npx @maixio/pstore --help
29
+ ```
30
+
31
+ Install globally:
32
+
33
+ ```bash
34
+ bun add -g @maixio/pstore
35
+ npm i -g @maixio/pstore
36
+ pstore --help
37
+ ```
38
+
39
+ Use as a library:
40
+
41
+ ```bash
42
+ bun add @maixio/pstore
43
+ ```
44
+
45
+ ```ts
46
+ import { createPstoreClient } from "@maixio/pstore";
47
+
48
+ const pstore = createPstoreClient({ locale: "en-US" });
49
+
50
+ const game = await pstore.lookup("10014149", {
51
+ includeProducts: true,
52
+ });
53
+
54
+ const search = await pstore.search({
55
+ term: "elden ring",
56
+ locale: "en-US",
57
+ size: 10,
58
+ });
59
+
60
+ const upcoming = await pstore.browse({
61
+ window: "next-thirty-days",
62
+ sortBy: "conceptReleaseDate",
63
+ order: "asc",
64
+ size: 10,
65
+ });
66
+
67
+ const deals = await pstore.categoryGrid({
68
+ id: "all-deals",
69
+ size: 10,
70
+ });
71
+
72
+ const status = await pstore.status();
73
+ ```
74
+
75
+ ## Public API
76
+
77
+ | API | Description |
78
+ | --- | --- |
79
+ | `createPstoreClient(options)` | Creates a small SDK facade with a default locale. |
80
+ | `lookupGame(id, options)` | Looks up a Concept ID or Product ID. |
81
+ | `searchGames(options)` | Searches the store and returns lightweight fields available from PSN Search. |
82
+ | `fetchBrowseGrid(query)` | Browses the store by time window, sorting, pagination, and filters. |
83
+ | `fetchCategoryGridResult(params)` | Browses a catalog/category UUID or built-in alias and returns pagination, sort, and facet metadata. |
84
+ | `fetchPsnStatus(options)` | Reads PlayStation Network service status. |
85
+ | `discoverCatalogs(options)` | Discovers catalog UUIDs from the current storefront pages. |
86
+ | `validateCatalogs(options)` | Validates whether built-in catalog UUIDs are still reachable. |
87
+ | `clearPstoreCaches()` | Clears process-local lookup/search/category caches. |
88
+
89
+ ## API Notes
90
+
91
+ - `lookupGame` detects Concept IDs and Product IDs from the ID shape.
92
+ - `searchGames` intentionally avoids hidden N+1 enrichment by default. It does not fetch publisher, release date, genres, or localized titles unless you use `lookup`.
93
+ - `fetchCategoryGridResult` accepts raw UUIDs. The SDK facade methods `category` and `categoryGrid` also accept built-in aliases.
94
+ - `createPstoreClient({ locale })` stores only a default locale. Caches are process-local and shared.
95
+ - The upstream PSN Web API has no public stability guarantee. Fields and persisted query hashes may drift when the web store changes.
96
+
97
+ ## CLI Overview
98
+
99
+ ```bash
100
+ pstore lookup --help
101
+ pstore search --help
102
+ pstore category --help
103
+ pstore browse --help
104
+ pstore catalogs
105
+ pstore status
106
+ pstore config
107
+ pstore init
108
+ pstore clear-cache
109
+ ```
110
+
111
+ ## Output Fields
112
+
113
+ Text and JSON output are projected from the same English field keys. `--json` only changes rendering. `--fields` limits output fields. `--select` limits output fields and, where possible, chooses a smaller lookup request plan.
114
+
115
+ Common fields:
116
+
117
+ - Lookup detail: `id`, `idType`, `conceptId`, `productId`, `name`, `locale`, `publisher`, `edition`, `releaseDate`, `price`, `rating`, `classification`, `topCategory`, `platforms`, `npTitleId`, `warnings`
118
+ - Full lookup detail: `productName`, `genres`, `skus`, `descriptions`, `media`, `products`, `contentRating`, `localizedTitles`, `concept`, `sources`, `timing`
119
+ - Search/category/browse default list fields: `id`, `name`, `type`, `price`, `platforms`, `classification`
120
+ - Optional list field: `url`
121
+
122
+ `search`, `category`, and `browse` do not output `url` by default because `id` is enough for `lookup` and scripts. Add it explicitly when needed:
123
+
124
+ ```bash
125
+ pstore search astro --json --fields id,name,price,url
126
+ pstore category all-deals --json --fields id,name,url
127
+ pstore browse --window next-thirty-days --json --fields id,name,url
128
+ ```
129
+
130
+ `lookup --select` can reduce upstream requests. For example, `--select id,name,price` skips rating, related products, and Batarang page requests. Selecting `publisher`, `descriptions`, or `contentRating` enables Batarang. For `search`, `category`, and `browse`, `--select` currently behaves like `--fields`.
131
+
132
+ ## lookup
133
+
134
+ Looks up game details by Concept ID or Product ID. Comma-separated batch lookup is supported.
135
+
136
+ Options:
137
+
138
+ - `-l, --locale <locale>`: locale, defaulting to config
139
+ - `-f, --fields <fields>`: comma-separated output fields
140
+ - `--select <fields>`: field-driven request plan and output projection
141
+ - `--full`: full detail profile with related products, concept data, and Batarang details
142
+ - `-p, --products`: include related editions/products
143
+ - `--with-concept`: include concept/default product data for Product ID lookups
144
+ - `-b, --batarang`: force Batarang HTML extraction for descriptions, publisher, and page metadata
145
+ - `--title-locales <locales>`: fetch localized titles; supports `cjk-en` or comma-separated locales
146
+ - `-j, --json`: JSON output
147
+ - `--verbose`: include timing information in text output
148
+ - `--concurrency <count>`: batch lookup concurrency, default `3`
149
+
150
+ Examples:
151
+
152
+ ```bash
153
+ pstore lookup 10014149
154
+ pstore lookup UP1001-PPSA28420_00-NBA2K26000000000
155
+ pstore lookup 10014149,231761
156
+ pstore lookup 10014149 --locale en-US
157
+ pstore lookup 10014149 --json
158
+ pstore lookup 10014149 --products
159
+ pstore lookup 10014149 --title-locales cjk-en --json
160
+ pstore lookup 10014149,10005069,10017939 --products --concurrency 2 --json
161
+ pstore lookup 10014149 --fields id,name,price,rating
162
+ pstore lookup 10014149 --select id,name,price --json
163
+ ```
164
+
165
+ Lookup keeps PSN object semantics:
166
+
167
+ - Product ID lookup uses `productRetrieve`; the top-level `price` belongs to that product/edition.
168
+ - Concept ID lookup uses `conceptRetrieve`; the top-level `price` belongs to the concept/default product.
169
+ - Product ID results expose the owning `conceptId`; use `--with-concept` when you need concept/default product details.
170
+ - `--products` returns related editions under the same concept, and each `products[].price` remains that edition's own price.
171
+
172
+ ## category
173
+
174
+ Browses a category by UUID or built-in alias.
175
+
176
+ Options:
177
+
178
+ - `-l, --locale <locale>`: locale
179
+ - `-p, --page <page>`: page number, default `1`
180
+ - `-s, --size <size>`: page size, default `24`
181
+ - `--sort <sort>`: sort alias or raw field direction
182
+ - `--filter <facet:key>`: generic facet filter, repeatable
183
+ - `--platform <platform>`: platform filter, repeatable
184
+ - `--genre <genre>`: genre filter, repeatable
185
+ - `--classification <classification>`: product classification filter
186
+ - `--price <range>`: price range, for example `0-0`
187
+ - `--release <window>`: release window, for example `last_thirty_days`
188
+ - `--vr <compatibility>`: VR compatibility filter
189
+ - `--facets`: include supported facet metadata
190
+ - `--sorts`: include supported sort metadata
191
+ - `-f, --fields <fields>`: comma-separated output fields
192
+ - `--select <fields>`: currently equivalent to `--fields`
193
+ - `-j, --json`: JSON output
194
+
195
+ Supported sort aliases:
196
+
197
+ - `best-selling`
198
+ - `downloads`
199
+ - `name`
200
+ - `name-desc`
201
+ - `price-asc`
202
+ - `price-desc`
203
+ - `release-new`
204
+ - `release-old`
205
+ - `field:asc` / `field:desc`
206
+
207
+ Examples:
208
+
209
+ ```bash
210
+ pstore category pre-orders
211
+ pstore category action
212
+ pstore category rpg
213
+ pstore category sports
214
+ pstore category simulation
215
+ pstore category all-deals
216
+ pstore category 3bf499d7-7acf-4931-97dd-2667494ee2c9
217
+ pstore category pre-orders --sort price-asc
218
+ pstore category all-deals --size 3 --facets --sorts
219
+ pstore category all-deals --platform PS5 --genre ACTION --price 0-0 --sort downloads
220
+ pstore category pre-orders --page 2 --size 12
221
+ pstore category pre-orders --json
222
+ pstore category all-deals --fields id,name,url --json
223
+ ```
224
+
225
+ ## browse
226
+
227
+ Browses PlayStation Store list semantics by time window, sorting, pagination, sampling, and generic filters.
228
+
229
+ Options:
230
+
231
+ - `-w, --window <window>`: `next-thirty-days`, `next_thirty_days`, `last-thirty-days`, `last_thirty_days`, or `all`
232
+ - `--sort <sort>`: `conceptReleaseDate`, `sales30`, `downloads30`, `conceptName`, `webBasePrice`, or `contentCollections.contentCollectionStartDate`
233
+ - `--order <order>`: `asc` or `desc`
234
+ - `-p, --page <page>`: page number, default `1`
235
+ - `-s, --size <size>`: page size, default `24`
236
+ - `--sample <count>`: random sample count
237
+ - `--filter <facet:value>`: generic raw PSN facet filter, repeatable
238
+ - `--platform <value>`: platform filter, for example `PS5`
239
+ - `--genre <value>`: genre filter, for example `ACTION`
240
+ - `--price <range>`: price range, for example `0-0` or `10000-19999`
241
+ - `--classification <value>`: classification filter, for example `FULL_GAME`
242
+ - `--age-rating <value>`: age rating filter
243
+ - `--vr <value>`: VR compatibility filter
244
+ - `--notice <value>`: compatibility notice filter
245
+ - `-f, --fields <fields>`: comma-separated output fields
246
+ - `--select <fields>`: currently equivalent to `--fields`
247
+ - `-l, --locale <locale>`: locale
248
+ - `-j, --json`: JSON output
249
+
250
+ Examples:
251
+
252
+ ```bash
253
+ pstore browse --window next-thirty-days --sort conceptReleaseDate --order asc --sample 3
254
+ pstore browse --window next-thirty-days --sort sales30 --order desc --sample 3
255
+ pstore browse --window last-thirty-days --sort conceptReleaseDate --order desc --sample 3
256
+ pstore browse --window last-thirty-days --sort sales30 --order desc --sample 3
257
+ pstore browse --platform PS5 --genre ACTION --price 0-0
258
+ pstore browse --filter conceptGenres:SPORTS --filter webBasePrice:10000-19999
259
+ pstore browse --fields id,name,url --json
260
+ ```
261
+
262
+ ## search
263
+
264
+ Searches games in the PlayStation Store.
265
+
266
+ `search` returns only stable lightweight fields that are present in the PSN Search response. It does not enrich each result with detail calls. For publisher, release date, genres, localized titles, descriptions, or ratings, take an `id` from search results and call `lookup <id>`.
267
+
268
+ Options:
269
+
270
+ - `-l, --locale <locale>`: locale
271
+ - `-p, --page <page>`: page number, default `1`
272
+ - `-s, --size <size>`: page size, default `24`
273
+ - `-f, --fields <fields>`: comma-separated output fields
274
+ - `--select <fields>`: currently equivalent to `--fields`
275
+ - `-j, --json`: JSON output
276
+
277
+ Examples:
278
+
279
+ ```bash
280
+ pstore search diablo
281
+ pstore search "elden ring" --locale en-US
282
+ pstore search "elden ring" --json
283
+ pstore search "elden ring" --json --fields id,name,price,url
284
+ pstore search "elden ring" --fields id,name,url
285
+ pstore search game --page 2
286
+ ```
287
+
288
+ ## catalogs
289
+
290
+ Lists built-in category aliases and UUIDs by default. The built-in table contains 58 relatively stable catalog UUIDs: 57 currently discoverable from storefront pages plus the hidden `all-deals` catalog. They are still upstream storefront data, so validate critical workflows before relying on them.
291
+
292
+ Live discovery is also available:
293
+
294
+ ```bash
295
+ pstore catalogs
296
+ pstore catalogs --live --depth 1 --limit 50
297
+ pstore catalogs --validate all-deals
298
+ pstore catalogs --validate --json
299
+ ```
300
+
301
+ In live output, `[from latest]` means the UUID was discovered from the `latest` navigation source page. It is a source marker, not the catalog title.
302
+
303
+ ## status
304
+
305
+ Reads public JSON data from [status.playstation.com](https://status.playstation.com) to report PlayStation Network service status.
306
+
307
+ ```bash
308
+ pstore status
309
+ pstore status --locale en-US
310
+ pstore status --country JP --json
311
+ ```
312
+
313
+ ## config / init
314
+
315
+ ```bash
316
+ pstore config
317
+ pstore init
318
+ pstore clear-cache
319
+ bun run live-smoke
320
+ ```
321
+
322
+ Global color option:
323
+
324
+ ```bash
325
+ pstore lookup 10014149 --no-color
326
+ export NO_COLOR=1
327
+ ```
328
+
329
+ ## Supported Locales
330
+
331
+ | Locale | Language | Region | Currency |
332
+ | --- | --- | --- | --- |
333
+ | `zh-hans-HK` | Simplified Chinese | Hong Kong | HK$ |
334
+ | `zh-hant-HK` | Traditional Chinese | Hong Kong | HK$ |
335
+ | `en-HK` | English | Hong Kong | HK$ |
336
+ | `zh-hant-TW` | Traditional Chinese | Taiwan | NT$ |
337
+ | `en-TW` | English | Taiwan | NT$ |
338
+ | `ja-JP` | Japanese | Japan | ¥ |
339
+ | `en-US` | English | United States | $ |
340
+ | `en-GB` | English | United Kingdom | £ |
341
+ | `zh-hans-CN` | Simplified Chinese | Mainland China | ¥ |
342
+
343
+ ## Field Reference
344
+
345
+ CLI field names are English keys for scripts and SDK consumers.
346
+
347
+ ### Default Lookup Fields
348
+
349
+ | Field | Type | Source | Description |
350
+ | --- | --- | --- | --- |
351
+ | `id` | string | derived | Primary result ID. |
352
+ | `idType` | `"concept"` / `"product"` | derived | Result object type. |
353
+ | `conceptId` | string | telemetry/price/rating/upsell | Owning concept ID. Product ID lookups try to fill this as well. |
354
+ | `productId` | string | telemetry/price/rating/upsell | Current product ID or default product ID for a concept. |
355
+ | `name` | string | telemetry/batarang | Game title. |
356
+ | `locale` | string | input | Current locale. |
357
+ | `publisher` | string | batarang | Publisher. The default detail profile attempts to fetch it. |
358
+ | `edition` | string | telemetry | Edition name. |
359
+ | `releaseDate` | string | telemetry/rating/batarang | ISO 8601 release date. |
360
+ | `price` | object | price/telemetry | Top-level price. Product ID means product price; Concept ID means default product price. |
361
+ | `rating` | object | rating/batarang | Rating summary without `ratingsDistribution`. |
362
+ | `platforms` | string[] | telemetry/upsell/batarang | Platform list. |
363
+ | `genres` | string[] | telemetry/batarang | Localized genres. |
364
+ | `localizedTitles` | object | lookup by locale | Multi-locale titles. Defaults to the `cjk-en` preset. |
365
+ | `warnings` | object[] | runtime | Non-fatal data source failures. |
366
+
367
+ ### Full Lookup Fields
368
+
369
+ | Field | Type | Source | Description |
370
+ | --- | --- | --- | --- |
371
+ | `productName` | string | telemetry | Product-level title. |
372
+ | `skus` | object[] | telemetry | SKU data. |
373
+ | `descriptions` | object[] | batarang | Short description, long description, compatibility notices, and legal text. |
374
+ | `media` | object[] | upsell/batarang | Screenshots, videos, and storefront images. |
375
+ | `products` | object[] | upsell | Related editions under the same concept; each product keeps its own `price`. |
376
+ | `contentRating` | object | telemetry/batarang | Content rating. |
377
+ | `concept` | object | lookup | Extra concept/default product data for Product ID lookups with `--with-concept`. |
378
+ | `sources` | object | runtime | Source tracking for important fields. |
379
+ | `timing` | object | runtime | Per-source timings. |
380
+ | `npTitleId` | string | telemetry/batarang | PSN title id. It is not in the default detail profile; select it with `--fields npTitleId`. |
381
+
382
+ ### Search / Category / Browse List Fields
383
+
384
+ | Field | Type | Default | Source | Description |
385
+ | --- | --- | --- | --- | --- |
386
+ | `id` | string | Yes | search/category grid | Concept ID or Product ID; use with `type`. |
387
+ | `name` | string | Yes | search/category grid | Localized title. |
388
+ | `type` | `"concept"` / `"product"` | Yes | `__typename` | List item type. |
389
+ | `price` | string/null | Yes | search/category grid | Current display price. |
390
+ | `platforms` | string[]/null | Yes | search/category grid | Platform list. |
391
+ | `classification` | string/null | Yes | search/category grid | Localized product classification, such as full game or deluxe edition. |
392
+ | `url` | string | No | derived | Store URL; explicitly request it with `--fields id,name,url`. |
393
+
394
+ Use `lookup --batarang`, `lookup --full`, or a detail-oriented `lookup --select` when you need descriptions, publisher, ratings, or content rating. `search` avoids default enrichment to prevent slow N+1 request patterns.
395
+
396
+ ## Category Aliases
397
+
398
+ | Alias | UUID | Description |
399
+ | --- | --- | --- |
400
+ | `all-deals` | `3f772501-...` | All deals, hidden catalog |
401
+ | `all-games` | `28c9c2b2-...` | All games |
402
+ | `all-ps5-games` | `d0446d4b-...` | All PS5 games |
403
+ | `all-ps4-games` | `30e3fe35-...` | All PS4 games |
404
+ | `pre-orders` | `3bf499d7-...` | Pre-orders |
405
+ | `add-ons` | `51c9aa7a-...` | Add-ons |
406
+ | `free-to-play` | `4dfd67ab-...` | Free-to-play |
407
+ | `best-sellers` | `132bb05e-...` | Best sellers |
408
+ | `new-games` | `e1699f77-...` | New games |
409
+ | `action` | `298b428c-...` | Action games |
410
+ | `rpg` | `e0b1cde3-...` | Role-playing games |
411
+ | `shooter` | `64ee024b-...` | Shooter games |
412
+ | `sports` | `b86f0f65-...` | Sports games |
413
+ | `racing` | `b78c6346-...` | Racing games |
414
+ | `fighting` | `0f6fc626-...` | Fighting games |
415
+ | `simulation` | `bb42a4e0-...` | Simulation games |
416
+
417
+ The complete list is available through `pstore catalogs` and the exported `BUILTIN_CATALOGS`.
418
+
419
+ ## Configuration
420
+
421
+ pstore reads `pstore.config.json` as the default config file.
422
+
423
+ Lookup order:
424
+
425
+ 1. `./pstore.config.json`
426
+ 2. `./psn-config.json` for legacy compatibility
427
+ 3. `~/.config/pstore/config.json`
428
+ 4. `~/.config/psn-cli/config.json` for legacy compatibility
429
+
430
+ Default config:
431
+
432
+ ```json
433
+ {
434
+ "locale": "zh-hans-HK",
435
+ "format": "text",
436
+ "pageSize": 24,
437
+ "verbose": false,
438
+ "color": true,
439
+ "cacheTtl": {
440
+ "lookup": 300000,
441
+ "search": 120000,
442
+ "category": 120000
443
+ }
444
+ }
445
+ ```
446
+
447
+ Fields:
448
+
449
+ - `locale`: default locale.
450
+ - `format`: default output format, `text` or `json`.
451
+ - `pageSize`: default page size for `search`, `category`, and `browse`, from `1` to `100`.
452
+ - `verbose`: whether lookup text output includes timing by default.
453
+ - `color`: whether colored output is enabled by default.
454
+ - `cacheTtl.lookup/search/category`: in-memory cache TTL in milliseconds.
455
+
456
+ Generate a config file:
457
+
458
+ ```bash
459
+ pstore init
460
+ ```
461
+
462
+ ## Technical Notes
463
+
464
+ - Uses PSN GraphQL persisted query APIs without login.
465
+ - Detail lookup combines telemetry, price, rating, upsell, and optional Batarang HTML sources.
466
+ - Product IDs map to PSN `productRetrieve`; the top-level price is the current product/edition CTA price.
467
+ - Concept IDs map to PSN `conceptRetrieve`; the top-level price is the default product CTA price.
468
+ - `conceptRetrieveForUpsellWithCtas` returns multiple editions/products, and pstore preserves each `products[].price`.
469
+ - HTTP timeout is 10 seconds, with 2 default retries. `429` and `5xx` responses back off and respect `Retry-After`.
470
+ - Batch lookup defaults to 3 concurrent requests and can be tuned with `--concurrency`.
471
+ - Process-local TTL caching covers lookup, search, and category calls.
472
+ - Batarang HTML data is extracted from SSR pages without a browser.
473
+ - Product ID results keep the product as the primary object and expose `conceptId`; use `--with-concept` for concept/default product details.
474
+
475
+ ## Scripting Tips
476
+
477
+ Prefer JSON output for automation:
478
+
479
+ ```bash
480
+ pstore lookup 10005069 --products --title-locales cjk-en --json
481
+ pstore search "Saros" --locale en-US --json --fields id,name,price,url
482
+ pstore browse --window next-thirty-days --sort conceptReleaseDate --order asc --sample 3 --json --fields id,name,price,url
483
+ ```
484
+
485
+ Recommendations:
486
+
487
+ - Always use `--json` and inspect `warnings` for partial data source failures.
488
+ - For product details, treat top-level `price` as that product's price. For concept details, treat top-level `price` as the default product price and `products[].price` as each edition's price.
489
+ - Use `--concurrency 2` or `--concurrency 3` for batch lookup to reduce the chance of `429` responses.
490
+ - Use `--title-locales cjk-en` when you need multi-locale titles.
491
+ - Use `--batarang` when you need descriptions, publisher, or content rating.
492
+ - Do not depend on text output order; depend on JSON keys.
493
+
494
+ ## Logs
495
+
496
+ API request logs are written to `logs/pstore-YYYY-MM-DD.log`, including request timings and outcomes.
497
+
498
+ ## Testing
499
+
500
+ ```bash
501
+ bun test
502
+ bunx tsc --noEmit
503
+ bun run live-smoke
504
+ bun run test:live
505
+ bun run test:live:locales
506
+ bun run test:live:full
507
+ PSTORE_SMOKE_LOCALES=zh-hans-HK,en-US,ja-JP,zh-hant-TW,en-TW bun run live-smoke
508
+ PSTORE_SMOKE_FULL=1 bun run live-smoke
509
+ ```
510
+
511
+ The test suite covers unit tests, API response fixtures, CLI/package contracts, performance checks, and edge cases. `live-smoke` checks the default locale by default. Use `PSTORE_SMOKE_LOCALES` and `PSTORE_SMOKE_FULL=1` to broaden live coverage when checking for persisted query hash or field shape drift.
512
+
513
+ ## License
514
+
515
+ MIT