@gscdump/cli 3.7.0 → 4.0.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 +113 -30
- package/bin/gscdump.mjs +21 -1
- package/dist/analysis-local.mjs +172 -53
- package/dist/auth-state.mjs +21 -6
- package/dist/auth.mjs +27 -18
- package/dist/bing-data.mjs +6 -2
- package/dist/bing-hosted.mjs +1 -1
- package/dist/cli-args.mjs +8 -0
- package/dist/cli.d.mts +5 -1
- package/dist/cli.mjs +41 -10
- package/dist/cloud-google.mjs +8 -1
- package/dist/command-meta.mjs +4 -4
- package/dist/command-registry.mjs +86 -10
- package/dist/commands/analyze.mjs +84 -43
- package/dist/commands/auth.mjs +46 -37
- package/dist/commands/bing.mjs +3 -3
- package/dist/commands/compact.mjs +12 -8
- package/dist/commands/config.mjs +14 -14
- package/dist/commands/doctor.mjs +26 -26
- package/dist/commands/dump.mjs +203 -105
- package/dist/commands/entities.mjs +10 -113
- package/dist/commands/gc.mjs +4 -3
- package/dist/commands/indexing-urls.mjs +185 -0
- package/dist/commands/indexing.mjs +12 -8
- package/dist/commands/init.mjs +77 -18
- package/dist/commands/inspect.mjs +97 -110
- package/dist/commands/mcp.mjs +37 -57
- package/dist/commands/papercut.mjs +1 -1
- package/dist/commands/profile.mjs +9 -17
- package/dist/commands/query.mjs +219 -210
- package/dist/commands/report.mjs +68 -85
- package/dist/commands/rollups.mjs +9 -6
- package/dist/commands/sitemaps.mjs +51 -104
- package/dist/commands/sites.mjs +40 -24
- package/dist/commands/stats.mjs +14 -17
- package/dist/commands/store-purge.mjs +9 -5
- package/dist/commands/store.mjs +0 -12
- package/dist/commands/sync.mjs +774 -296
- package/dist/config.mjs +7 -4
- package/dist/context.mjs +51 -21
- package/dist/coverage.mjs +251 -0
- package/dist/dump-bing.mjs +71 -0
- package/dist/dump-writers.mjs +246 -0
- package/dist/error-handler.mjs +108 -38
- package/dist/filters.mjs +125 -0
- package/dist/hosted-site.mjs +91 -0
- package/dist/inspect-urls.mjs +76 -0
- package/dist/inspection-record.mjs +93 -0
- package/dist/local-entities.mjs +658 -0
- package/dist/mcp/errors.mjs +71 -18
- package/dist/mcp/handlers/diagnostics.mjs +15 -12
- package/dist/mcp/handlers/reports.mjs +33 -42
- package/dist/mcp/server/index.mjs +80 -38
- package/dist/mcp/types.mjs +7 -7
- package/dist/package.mjs +1 -1
- package/dist/quota-ledger.mjs +370 -0
- package/dist/render/analysis.mjs +8 -1
- package/dist/render/report.mjs +8 -1
- package/dist/request-pacer.mjs +29 -0
- package/dist/route.mjs +295 -0
- package/dist/sitemap.mjs +4 -0
- package/dist/sql-views.mjs +117 -0
- package/dist/store-sites.mjs +134 -0
- package/dist/sync-plan.mjs +129 -0
- package/dist/sync-run.mjs +96 -0
- package/dist/table-sources.mjs +57 -0
- package/dist/token-info.mjs +54 -0
- package/dist/utils.mjs +28 -19
- package/dist/window.mjs +159 -0
- package/package.json +17 -14
- package/skills/gscdump/SKILL.md +167 -37
- package/dist/commands/export.mjs +0 -76
- package/dist/native-duckdb.mjs +0 -46
package/README.md
CHANGED
|
@@ -28,13 +28,13 @@ gscdump auth login --mode local
|
|
|
28
28
|
gscdump sites
|
|
29
29
|
|
|
30
30
|
# Sync 90 days to the Store
|
|
31
|
-
gscdump sync --site
|
|
31
|
+
gscdump sync --site example.com --days 90 --tables pages,queries,page_queries,countries,dates
|
|
32
32
|
|
|
33
33
|
# Query the Store
|
|
34
|
-
gscdump query --site
|
|
34
|
+
gscdump query --site example.com --dimensions page,query --limit 50
|
|
35
35
|
|
|
36
36
|
# Run an Analyzer
|
|
37
|
-
gscdump analyze striking-distance --site
|
|
37
|
+
gscdump analyze striking-distance --site example.com
|
|
38
38
|
|
|
39
39
|
# Start the MCP server
|
|
40
40
|
gscdump mcp
|
|
@@ -44,7 +44,7 @@ gscdump mcp
|
|
|
44
44
|
|
|
45
45
|
| Command | Description |
|
|
46
46
|
|---|---|
|
|
47
|
-
| `init` | Full setup (OAuth + dataDir; offers to write a `.env` for later use) |
|
|
47
|
+
| `init` | Full setup (OAuth + dataDir; offers to write a `.env` for later use). `--mode cloud\|local` saves the auth mode. Without a terminal it never prompts |
|
|
48
48
|
| `auth` | Manage authentication (`status`, `login`, `logout`, `refresh`) |
|
|
49
49
|
| `bing` | Connect Bing, list sites, dump datasets, inspect URLs, and check hosted verification |
|
|
50
50
|
| `config` | Manage CLI configuration (`show`, `set`, `unset`, `path`, `validate`) |
|
|
@@ -52,18 +52,17 @@ gscdump mcp
|
|
|
52
52
|
| `sites [--owner-only] [--with-sitemaps]` | List available GSC sites |
|
|
53
53
|
| `sites add <url>` / `sites delete <url> [--yes]` | Register or remove a Site in Search Console (add registers in unverified state) |
|
|
54
54
|
| `sites verify-token <url> [--method]` / `sites verify <url> [--method]` | Get a verification token, then trigger ownership verification (META/FILE/DNS_TXT/DNS_CNAME/ANALYTICS/TAG_MANAGER) |
|
|
55
|
-
| `sitemaps` | List, submit, or delete Google sitemaps; probe live URLs; read hosted snapshots (`current`, `history`, `membership`, `lastmod`, `export`) |
|
|
56
|
-
| `inspect <url
|
|
57
|
-
| `indexing` | Notify Google about URL changes (`submit`, `remove`, `status`, `batch`, `batch-status`, `quota`); supports `--retries` |
|
|
58
|
-
| `sync` | Sync GSC data to the local
|
|
59
|
-
| `query` | Run a search analytics query (Store
|
|
60
|
-
| `dump` | Export
|
|
61
|
-
| `analyze <tool>` | Run an SEO Analyzer against the Store (`--live` for row-based
|
|
62
|
-
| `entities` |
|
|
55
|
+
| `sitemaps` | List, submit, or delete Google sitemaps; probe live URLs; read hosted snapshots (`current`, `history`, `membership`, `lastmod`, `export`) with `--site` |
|
|
56
|
+
| `inspect <url...> [--file]` | URL inspection for one or more URLs; renders Indexing Evidence, rich results, and AMP, and saves each result to the Store |
|
|
57
|
+
| `indexing` | Notify Google about URL changes (`submit`, `remove`, `status`, `batch`, `batch-status`, `quota`); supports `--retries`. `indexing urls --status not_indexed` lists hosted URL Inspection results |
|
|
58
|
+
| `sync` | Sync GSC data, sitemaps, and URL Inspection results to the local Store; `--inspect-limit`, `--max-calls`, `--all-sites`, `--no-sitemaps`, `--no-inspections`, `--retry-failed`, `--dry-run` |
|
|
59
|
+
| `query` | Run a search analytics query (the Store when it covers the dates, see [Routing](#routing); `--live` hits GSC API). Filters: `--query`, `--page`, `--country`, `--device`, `--search-appearance`, `--type`, `--data-state`, `--aggregation-type`. `--explain` previews the request body; `--output -` writes to stdout. `--sql` runs DuckDB SQL over the Store views; `--schema` lists them. |
|
|
60
|
+
| `dump` | Export the Store, inspections, sitemaps, and Bing data (`--format parquet\|csv\|json\|ndjson\|sqlite\|duckdb`, `--tables`, `--all-sites`, `--no-bing`). Every row has `site` and `search_type` |
|
|
61
|
+
| `analyze <tool>` | Run an SEO Analyzer against the Store, or live when the Site has no Store data (`--live` forces live for row-based Analyzers) |
|
|
62
|
+
| `entities` | Read saved URL inspections and snapshot indexing metadata into the local entity store |
|
|
63
63
|
| `store stats` | Show row/byte counts per table and on-disk footprint |
|
|
64
64
|
| `store compact` | Compact older data into weekly, monthly, and quarterly tiers (`--dry-run`) |
|
|
65
65
|
| `store gc` | Delete orphaned objects past the grace window (`--dry-run`) |
|
|
66
|
-
| `store export` | Export the live store to a single `.duckdb` file |
|
|
67
66
|
| `store rm-site` / `store reset` | Delete one Site's data or reset the Store; inspect `--help` before use |
|
|
68
67
|
| `store rollups rebuild` | Rebuild post-sync rollup tables |
|
|
69
68
|
| `report <id>` / `report list` | Run or list Reports; `--explain` previews a plan |
|
|
@@ -85,7 +84,7 @@ gscdump auth status --json
|
|
|
85
84
|
|
|
86
85
|
# Google uses the connection on gscdump.com
|
|
87
86
|
gscdump sites --json
|
|
88
|
-
gscdump query --live --site
|
|
87
|
+
gscdump query --live --site example.com --dimensions page
|
|
89
88
|
|
|
90
89
|
# Connect Bing through gscdump.com, then read its data
|
|
91
90
|
gscdump bing sites --json
|
|
@@ -117,6 +116,7 @@ If you change a saved API root, supply the API key explicitly.
|
|
|
117
116
|
| Bing connection and CNAME verification | Uses `bing login`, `bing status`, and `bing verify` | Verify sites in Bing Webmaster Tools |
|
|
118
117
|
| Hosted sitemap membership and history | Supported | Requires hosted authentication |
|
|
119
118
|
| Store queries and exports | Reads the local Store | Reads the local Store |
|
|
119
|
+
| Hosted sync progress | `status` and `sites` show it | Not available |
|
|
120
120
|
|
|
121
121
|
Hosted Bing access follows the API's plan and preview access rules.
|
|
122
122
|
`auth logout` removes the saved mode and saved Google and Bing credentials.
|
|
@@ -178,7 +178,7 @@ Bing Indexing Evidence preserves uncertainty and does not imply an indexed verdi
|
|
|
178
178
|
|
|
179
179
|
```bash
|
|
180
180
|
# pages under /blog/ with brand mentions in the query
|
|
181
|
-
gscdump query --live --site
|
|
181
|
+
gscdump query --live --site example.com \
|
|
182
182
|
--page '~/blog/' --query '~brand' --dimensions page,query
|
|
183
183
|
```
|
|
184
184
|
|
|
@@ -186,16 +186,64 @@ gscdump query --live --site sc-domain:example.com \
|
|
|
186
186
|
Without saved values, queries use 1000 rows and JSON.
|
|
187
187
|
Limits must be positive integers. Formats must be `json` or `csv`.
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
Without dates, a query reads the last 28 days that end on the newest synced day of the table it reads.
|
|
190
|
+
With `--live`, the window ends three days ago, Pacific time: the newest final GSC date.
|
|
191
|
+
`--start` alone runs to that end date. `--end` alone reads the 28 days that end on it.
|
|
191
192
|
Dates must use `YYYY-MM-DD`, and `--start` cannot follow `--end`.
|
|
192
193
|
|
|
194
|
+
`--page` accepts a path or a full URL. The Store compares paths, so `https://example.com/a` matches `/a`.
|
|
195
|
+
Live queries expand a path to the Site's origin. For a domain property, a path matches that path on any host.
|
|
196
|
+
Filtered dimensions choose the Store table too: `-d query --page /a` reads `page_queries`.
|
|
197
|
+
If no Store table holds every dimension and filter, the query fails and names `--live`.
|
|
198
|
+
|
|
193
199
|
```bash
|
|
194
200
|
# Export query rows with a CSV header.
|
|
195
|
-
gscdump query --live --site
|
|
201
|
+
gscdump query --live --site example.com \
|
|
196
202
|
--dimensions page,query --format csv --output rows.csv
|
|
197
203
|
```
|
|
198
204
|
|
|
205
|
+
### SQL over the Store
|
|
206
|
+
|
|
207
|
+
`query --sql` runs DuckDB SQL over one view per Store table.
|
|
208
|
+
Run `query --schema` to list the views, their columns, and their date ranges.
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
gscdump query --format json --sql "
|
|
212
|
+
SELECT p.page, SUM(q.clicks) AS clicks, gsc_position(q.sum_position, q.impressions) AS position
|
|
213
|
+
FROM pages p JOIN page_queries q USING (site, search_type, url, date)
|
|
214
|
+
WHERE p.search_type = 'web' AND p.date >= DATE '2026-08-01'
|
|
215
|
+
GROUP BY p.page ORDER BY clicks DESC LIMIT 20"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
| Column | Meaning |
|
|
219
|
+
| --- | --- |
|
|
220
|
+
| `site` | The Site URL, such as `sc-domain:example.com` |
|
|
221
|
+
| `search_type` | `web`, `image`, `video`, `news`, `discover`, or `googleNews` |
|
|
222
|
+
| `url`, `page` | The page path. `page` is the same value as `url` |
|
|
223
|
+
| `sum_position` | Zero-based position multiplied by impressions |
|
|
224
|
+
|
|
225
|
+
- The Store keeps every search type. Filter or group by `search_type`, or a `SUM` adds web, image, and Discover rows together.
|
|
226
|
+
- `gsc_position(sum_position, impressions)` returns the impression-weighted average position. Use it with `GROUP BY`.
|
|
227
|
+
- The views cover every Site in the Store. `--site` and `--type` narrow them.
|
|
228
|
+
- Dates return as `YYYY-MM-DD`. Integers return as numbers, and an integer past 2^53 returns as a string.
|
|
229
|
+
- If the SQL names a table with no synced data, the CLI prints a warning.
|
|
230
|
+
|
|
231
|
+
### Export formats
|
|
232
|
+
|
|
233
|
+
`dump` reads only the Store. It never calls Google to fill a gap.
|
|
234
|
+
Every exported row has `site` and `search_type` columns.
|
|
235
|
+
|
|
236
|
+
| `--format` | Output |
|
|
237
|
+
| --- | --- |
|
|
238
|
+
| `parquet` (default) | `<site>/<search_type>/<table>.parquet` |
|
|
239
|
+
| `csv`, `json`, `ndjson` | `<site>/<search_type>/<table>.<ext>`, plus a per-row `position` |
|
|
240
|
+
| `sqlite` | One `gscdump.sqlite` file. Dates are `YYYY-MM-DD` text |
|
|
241
|
+
| `duckdb` | One `gscdump.duckdb` file |
|
|
242
|
+
|
|
243
|
+
Inspections, sitemaps, and Indexing API metadata go to `<site>/<dataset>.<ext>`, or to their own tables in a database file.
|
|
244
|
+
`manifest.json` lists every dataset with its row count, and the same coverage that `sync --status` reports.
|
|
245
|
+
`--format sqlite` needs Node.js 22.13 or later.
|
|
246
|
+
|
|
199
247
|
### Global flags
|
|
200
248
|
|
|
201
249
|
- `--no-color` / `NO_COLOR` env: strip ANSI from stdout (stderr keeps colour for interactive use).
|
|
@@ -212,38 +260,73 @@ Saved config rejects invalid values and unknown keys. If parsing fails, fix the
|
|
|
212
260
|
`gscdump analyze <tool>` runs one of 29 Analyzers from `@gscdump/analysis`.
|
|
213
261
|
See the [full list](../../README.md#analyzers) and [Source support](../analysis/README.md#sources).
|
|
214
262
|
|
|
215
|
-
Each Analyzer accepts `--site`, `--start`, `--end`, `--limit`, and output flags.
|
|
216
|
-
|
|
263
|
+
Each Analyzer accepts `--site`, `--period`, `--start`, `--end`, `--limit`, `--fetch-budget`, and output flags.
|
|
264
|
+
Windows default to the last 28 days that end on the newest synced day.
|
|
265
|
+
`movers` and `decay` compare with the previous period by default. Override it with `--prev-start` and `--prev-end` together.
|
|
266
|
+
`movers` also accepts `--sort-by`: `clicksDelta` (default) sorts by absolute click change, `clicksDeltaPercent` by percent change.
|
|
267
|
+
`--limit` caps the rows shown. `--fetch-budget` caps the rows each live fetch reads (default 25000, max 100000).
|
|
268
|
+
If a fetch reaches the budget, the output shows a partial-data warning.
|
|
217
269
|
Use `gscdump analyze <tool> --help` for additional options.
|
|
218
270
|
|
|
219
|
-
|
|
271
|
+
`analyze` and `report` follow the [routing rules](#routing).
|
|
220
272
|
Pass `--live` to use Google explicitly.
|
|
221
273
|
SQL-only Analyzers require local data.
|
|
222
274
|
|
|
275
|
+
## Routing
|
|
276
|
+
|
|
277
|
+
Login is optional. Log in with Google or a hosted key, or sync a local Store.
|
|
278
|
+
`query`, `analyze`, and `report` pick one source for each run:
|
|
279
|
+
|
|
280
|
+
- If the Store covers every date the run needs, the run reads the Store. This is also true while a sync runs.
|
|
281
|
+
- If the Store has no data for the tables the run needs, and Google is connected, the run asks the live Search Console API.
|
|
282
|
+
stderr prints `No synced data for SITE; answering from the live Search Console API.` JSON output has `meta.source: "live"`.
|
|
283
|
+
- If the Store has no data and Google is not connected, the run stops and names `gscdump init`.
|
|
284
|
+
- If some dates are missing, the run stops and prints the `gscdump sync` command for the missing dates and tables. Pass `--live` to ask Google instead.
|
|
285
|
+
- If a sync for the Site is running and the dates are not covered yet, the run stops with `Sync running: 41 of 90 days done.`
|
|
286
|
+
A sync without a heartbeat in the last 2 minutes counts as stopped.
|
|
287
|
+
|
|
288
|
+
One run never mixes Store rows and live rows. Live results hold the top rows of each request, and synced data keeps more of the long tail.
|
|
289
|
+
`query --sql` reads the Store only.
|
|
290
|
+
With `--format json` or `--json`, a stop prints `{ "error": { "code", "message", "nextCommand" } }` on stdout and exits 1.
|
|
291
|
+
|
|
292
|
+
Every Search Analytics, URL Inspection, and Indexing API call goes through the quota ledger in the data dir, so all commands share one daily budget.
|
|
293
|
+
When a quota is spent, the command stops and says when the quota resets.
|
|
294
|
+
|
|
223
295
|
## Sync
|
|
224
296
|
|
|
225
297
|
```bash
|
|
226
|
-
# Default:
|
|
227
|
-
|
|
298
|
+
# Default: catch every table and search type up to the latest final date.
|
|
299
|
+
# A table with no history starts 28 days back. Newest dates come first.
|
|
300
|
+
# Also saves sitemaps and inspects up to 50 due URLs. Skips completed dates.
|
|
301
|
+
gscdump sync --site example.com
|
|
302
|
+
|
|
303
|
+
# Backfill the 16 months Google keeps, plus 14 days Google often still serves
|
|
304
|
+
gscdump sync --site example.com --full
|
|
305
|
+
|
|
306
|
+
# Cap one run at 2,000 Search Analytics calls; the next run continues
|
|
307
|
+
gscdump sync --site example.com --full --max-calls 2000
|
|
228
308
|
|
|
229
|
-
#
|
|
230
|
-
gscdump sync --
|
|
309
|
+
# Every verified Site, one after another
|
|
310
|
+
gscdump sync --all-sites
|
|
231
311
|
|
|
232
312
|
# Custom range
|
|
233
|
-
gscdump sync --site
|
|
313
|
+
gscdump sync --site example.com --start 2026-08-01 --end 2026-08-31 \
|
|
234
314
|
--tables pages,queries,page_queries,countries,dates
|
|
235
315
|
|
|
236
|
-
#
|
|
237
|
-
gscdump sync --site
|
|
316
|
+
# Coverage, missing and failed dates per table, and a running sync
|
|
317
|
+
gscdump sync --site example.com --status
|
|
238
318
|
|
|
239
319
|
# Limit concurrent day requests per table
|
|
240
|
-
gscdump sync --site
|
|
320
|
+
gscdump sync --site example.com --concurrency 4 \
|
|
241
321
|
--tables pages,queries,page_queries,countries,dates
|
|
242
322
|
```
|
|
243
323
|
|
|
244
324
|
Sync skips completed dates; `--force` refreshes them.
|
|
325
|
+
A day Google still updates stays `pending`, and the next sync fetches it again.
|
|
326
|
+
Sync records Google calls in a quota ledger in the Store directory.
|
|
327
|
+
If Google refuses a call for quota, sync stops, keeps the rest `pending`, and exits 0. The next run continues.
|
|
245
328
|
Cross-process locks coordinate `sync`, `compact`, and `gc`.
|
|
246
|
-
Pagination follows Google's 25,000-row pages, subject to [Google's data limits](https://developers.google.com/webmaster-tools/v1/how-tos/all-your-data).
|
|
329
|
+
Pagination follows Google's 25,000-row pages and stops at the first short page, subject to [Google's data limits](https://developers.google.com/webmaster-tools/v1/how-tos/all-your-data).
|
|
247
330
|
|
|
248
331
|
## MCP server
|
|
249
332
|
|
package/bin/gscdump.mjs
CHANGED
|
@@ -2,7 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Node does not flush pending pipe writes on process.exit. Drain a stream
|
|
6
|
+
// first so a failed run cannot truncate piped output.
|
|
7
|
+
function drained(stream) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
stream.once('error', resolve)
|
|
10
|
+
if (stream.write(''))
|
|
11
|
+
resolve()
|
|
12
|
+
else
|
|
13
|
+
stream.once('drain', resolve)
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
import ('../dist/cli.mjs').then(async ({ runCli }) => {
|
|
18
|
+
const code = await runCli()
|
|
19
|
+
// A failed command may leave handles open (DuckDB, sockets). Exit now.
|
|
20
|
+
// On success the process ends by itself, so a server command stays up.
|
|
21
|
+
if (code !== 0) {
|
|
22
|
+
await Promise.all([drained(process.stdout), drained(process.stderr)])
|
|
23
|
+
process.exit(code)
|
|
24
|
+
}
|
|
25
|
+
}).catch((error) => {
|
|
6
26
|
console.error(error instanceof Error ? error.message : String(error))
|
|
7
27
|
process.exitCode = 1
|
|
8
28
|
})
|
package/dist/analysis-local.mjs
CHANGED
|
@@ -1,33 +1,127 @@
|
|
|
1
|
+
import { LocalStoreUnsupportedError } from "./error-handler.mjs";
|
|
2
|
+
import { useCliRuntime } from "./runtime.mjs";
|
|
1
3
|
import { logger } from "./utils.mjs";
|
|
2
|
-
import {
|
|
4
|
+
import { inferTable } from "./local-store.mjs";
|
|
3
5
|
import { createCommandContext } from "./context.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { decideRoute, liveNote, readRouteState, readSiteStates, resolveReadSite, stopAtRoute } from "./route.mjs";
|
|
7
|
+
import { newestDoneDate } from "./window.mjs";
|
|
8
|
+
import { extractDateRange } from "gscdump/query";
|
|
9
|
+
import { getLatestGscDate } from "gscdump/dates";
|
|
10
|
+
import { err, ok, unwrapResult } from "gscdump/result";
|
|
8
11
|
import { defaultAnalyzerRegistry } from "@gscdump/analysis/registry";
|
|
9
12
|
import { createGscApiQuerySource } from "@gscdump/engine-gsc-api";
|
|
10
13
|
import { AnalyzerCapabilityError, runAnalyzerFromSource } from "@gscdump/engine/analyzer";
|
|
11
14
|
import { createEngineQuerySource } from "@gscdump/engine/source";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const PLACEHOLDER_DATE = "2000-01-01";
|
|
16
|
+
const DAILY_PARTITION_RE = /^daily\/(\d{4}-\d{2}-\d{2})$/;
|
|
17
|
+
function sqlPlanReads(params) {
|
|
18
|
+
const analyzer = defaultAnalyzerRegistry.getAnalyzerVariants(params.type)?.sql;
|
|
19
|
+
if (!analyzer) return void 0;
|
|
20
|
+
let plan;
|
|
21
|
+
try {
|
|
22
|
+
plan = analyzer.build(params);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
logger.debug(`Cannot plan ${params.type}: ${error.message}`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (plan.kind !== "sql") return void 0;
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
period: "current",
|
|
31
|
+
fileSet: plan.current
|
|
32
|
+
},
|
|
33
|
+
...plan.previous ? [{
|
|
34
|
+
period: "comparison",
|
|
35
|
+
fileSet: plan.previous
|
|
36
|
+
}] : [],
|
|
37
|
+
...Object.values(plan.extraFiles ?? {}).map((fileSet) => ({
|
|
38
|
+
period: "current",
|
|
39
|
+
fileSet
|
|
40
|
+
}))
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
function analyzerTables(params) {
|
|
44
|
+
const reads = sqlPlanReads({
|
|
45
|
+
startDate: PLACEHOLDER_DATE,
|
|
46
|
+
endDate: PLACEHOLDER_DATE,
|
|
47
|
+
prevStartDate: PLACEHOLDER_DATE,
|
|
48
|
+
prevEndDate: PLACEHOLDER_DATE,
|
|
49
|
+
...params
|
|
50
|
+
});
|
|
51
|
+
return [...new Set((reads ?? []).map((read) => read.fileSet.table))];
|
|
52
|
+
}
|
|
53
|
+
function isAdapterPlanned(type) {
|
|
54
|
+
return defaultAnalyzerRegistry.getAnalyzerVariants(type)?.sql?.requires.includes("adapter") ?? false;
|
|
55
|
+
}
|
|
56
|
+
function isBuilderState(value) {
|
|
57
|
+
return Boolean(value) && typeof value === "object" && Array.isArray(value.dimensions);
|
|
19
58
|
}
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
59
|
+
function builderStateNeed(state, period) {
|
|
60
|
+
const table = inferTable(state.dimensions);
|
|
61
|
+
const { startDate, endDate } = extractDateRange(state.filter);
|
|
62
|
+
const searchType = state.searchType && state.searchType !== "web" ? state.searchType : void 0;
|
|
63
|
+
if (startDate && endDate) return {
|
|
64
|
+
kind: "window",
|
|
65
|
+
period,
|
|
66
|
+
table,
|
|
67
|
+
searchType: searchType ?? "web",
|
|
68
|
+
window: {
|
|
69
|
+
start: startDate,
|
|
70
|
+
end: endDate
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
kind: "any",
|
|
75
|
+
tables: [table],
|
|
76
|
+
...searchType ? { searchType } : {}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function builderStateNeeds(params) {
|
|
80
|
+
if (!isAdapterPlanned(params.type)) return [];
|
|
81
|
+
if (!isBuilderState(params.q)) return [{
|
|
82
|
+
kind: "any",
|
|
83
|
+
tables: []
|
|
84
|
+
}];
|
|
85
|
+
const needs = [builderStateNeed(params.q, "current")];
|
|
86
|
+
if (isBuilderState(params.qc)) needs.push(builderStateNeed(params.qc, "comparison"));
|
|
87
|
+
return needs;
|
|
88
|
+
}
|
|
89
|
+
function analysisNeeds(params) {
|
|
90
|
+
const needs = [];
|
|
91
|
+
for (const { period, fileSet } of sqlPlanReads(params) ?? []) {
|
|
92
|
+
const dates = fileSet.partitions.flatMap((partition) => DAILY_PARTITION_RE.exec(partition)?.[1] ?? []).sort();
|
|
93
|
+
if (dates.length === 0) {
|
|
94
|
+
needs.push({
|
|
95
|
+
kind: "any",
|
|
96
|
+
tables: [fileSet.table]
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
needs.push({
|
|
101
|
+
kind: "window",
|
|
102
|
+
period,
|
|
103
|
+
table: fileSet.table,
|
|
104
|
+
searchType: "web",
|
|
105
|
+
window: {
|
|
106
|
+
start: dates[0],
|
|
107
|
+
end: dates.at(-1)
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (needs.length > 0) return needs;
|
|
112
|
+
const builderNeeds = builderStateNeeds(params);
|
|
113
|
+
if (builderNeeds.length > 0) return builderNeeds;
|
|
114
|
+
return [{
|
|
115
|
+
kind: "any",
|
|
116
|
+
tables: []
|
|
117
|
+
}];
|
|
23
118
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return siteUrls.find((s) => s.includes(hint) || hint.includes(s)) ?? null;
|
|
119
|
+
function analyzerSources(types) {
|
|
120
|
+
const variants = types.map((type) => defaultAnalyzerRegistry.getAnalyzerVariants(type));
|
|
121
|
+
return {
|
|
122
|
+
local: variants.every((variant) => Boolean(variant?.sql)),
|
|
123
|
+
live: variants.every((variant) => Boolean(variant?.rows))
|
|
124
|
+
};
|
|
31
125
|
}
|
|
32
126
|
async function runAnalysisResult(source, params, mode) {
|
|
33
127
|
return runAnalyzerFromSource(source, params, defaultAnalyzerRegistry).then(ok).catch((e) => {
|
|
@@ -36,56 +130,81 @@ async function runAnalysisResult(source, params, mode) {
|
|
|
36
130
|
});
|
|
37
131
|
}
|
|
38
132
|
function makeRunAnalysis(source, mode) {
|
|
39
|
-
return async (params) =>
|
|
133
|
+
return async (params) => {
|
|
134
|
+
const result = unwrapResult(await runAnalysisResult(source, params, mode), (e) => e);
|
|
135
|
+
return {
|
|
136
|
+
...result,
|
|
137
|
+
meta: {
|
|
138
|
+
...result.meta,
|
|
139
|
+
source: mode
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
};
|
|
40
143
|
}
|
|
41
144
|
async function resolveAnalysisSource(args) {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
145
|
+
const forceLive = Boolean(args.live);
|
|
146
|
+
const ctx = await createCommandContext({ needsStore: true });
|
|
147
|
+
const store = ctx.store;
|
|
148
|
+
let live;
|
|
149
|
+
const connect = () => live ??= createCommandContext({
|
|
150
|
+
needsAuth: true,
|
|
151
|
+
needsStore: false
|
|
152
|
+
});
|
|
153
|
+
const { site, siteHint, auth } = await resolveReadSite(ctx, args.site ? String(args.site) : void 0, {
|
|
154
|
+
forceLive,
|
|
155
|
+
connect
|
|
156
|
+
});
|
|
157
|
+
const states = site ? await readSiteStates(store, site) : [];
|
|
158
|
+
const anchor = forceLive ? getLatestGscDate() : newestDoneDate(states, args.anchorTables) ?? getLatestGscDate();
|
|
159
|
+
const sources = args.sources ?? analyzerSources(args.types);
|
|
160
|
+
const req = {
|
|
161
|
+
site,
|
|
162
|
+
siteHint,
|
|
163
|
+
label: args.label,
|
|
164
|
+
localCapable: sources.local,
|
|
165
|
+
liveCapable: sources.live,
|
|
166
|
+
forceLive,
|
|
167
|
+
argv: useCliRuntime().rawArgs
|
|
168
|
+
};
|
|
169
|
+
const state = await readRouteState({
|
|
170
|
+
store,
|
|
171
|
+
site,
|
|
172
|
+
needs: args.needs(anchor),
|
|
173
|
+
states,
|
|
174
|
+
auth
|
|
175
|
+
});
|
|
176
|
+
const route = decideRoute(req, state);
|
|
177
|
+
if (route.kind === "syncing" || route.kind === "prompt") stopAtRoute(route, req, state.auth, { json: Boolean(args.json) });
|
|
178
|
+
if (route.kind === "local") {
|
|
59
179
|
const source = createEngineQuerySource({
|
|
60
180
|
engine: store.engine,
|
|
61
181
|
ctx: {
|
|
62
182
|
userId: store.userId,
|
|
63
|
-
siteId: store.siteIdFor(
|
|
183
|
+
siteId: store.siteIdFor(site)
|
|
64
184
|
}
|
|
65
185
|
});
|
|
66
186
|
return {
|
|
67
187
|
source,
|
|
68
|
-
siteUrl,
|
|
69
|
-
|
|
70
|
-
|
|
188
|
+
siteUrl: site,
|
|
189
|
+
isLive: false,
|
|
190
|
+
anchor,
|
|
71
191
|
runAnalysis: makeRunAnalysis(source, "local")
|
|
72
192
|
};
|
|
73
193
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
});
|
|
78
|
-
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
194
|
+
const liveCtx = await connect();
|
|
195
|
+
const siteUrl = site ?? await liveCtx.resolveSite(void 0);
|
|
196
|
+
if (route.reason === "no-store-data") logger.warn(liveNote(siteUrl));
|
|
79
197
|
const source = createGscApiQuerySource({
|
|
80
|
-
client:
|
|
198
|
+
client: liveCtx.client,
|
|
81
199
|
siteUrl
|
|
82
200
|
});
|
|
83
201
|
return {
|
|
84
202
|
source,
|
|
85
203
|
siteUrl,
|
|
86
|
-
|
|
87
|
-
|
|
204
|
+
isLive: true,
|
|
205
|
+
liveReason: route.reason,
|
|
206
|
+
anchor,
|
|
88
207
|
runAnalysis: makeRunAnalysis(source, "live")
|
|
89
208
|
};
|
|
90
209
|
}
|
|
91
|
-
export {
|
|
210
|
+
export { analysisNeeds, analyzerSources, analyzerTables, resolveAnalysisSource };
|
package/dist/auth-state.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { HOSTED_KEY_REJECTED } from "./error-handler.mjs";
|
|
1
2
|
import { useCliRuntime } from "./runtime.mjs";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import fs from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
6
|
+
import { gscdumpAvailableSiteSchema } from "@gscdump/contracts";
|
|
5
7
|
import { z } from "zod";
|
|
6
8
|
const apiRootSchema = z.url().transform((value) => value.replace(/\/+$/, "")).refine((value) => {
|
|
7
9
|
const url = new URL(value);
|
|
@@ -97,11 +99,14 @@ async function cloudRequest(state, route, options = {}) {
|
|
|
97
99
|
signal: options.signal ?? AbortSignal.timeout(3e4),
|
|
98
100
|
redirect: "error"
|
|
99
101
|
});
|
|
100
|
-
if (!response.ok)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
const message = response.status === 401 ? HOSTED_KEY_REJECTED : `Hosted request failed (${response.status}) for ${route.split("?")[0]}. Check \`gscdump auth status\`.`;
|
|
104
|
+
throw Object.assign(new Error(message), {
|
|
105
|
+
statusCode: response.status,
|
|
106
|
+
retryAfter: response.headers.get("retry-after"),
|
|
107
|
+
response
|
|
108
|
+
});
|
|
109
|
+
}
|
|
105
110
|
return response.status === 204 ? void 0 : response.json();
|
|
106
111
|
}
|
|
107
112
|
async function getCloudAccount(state) {
|
|
@@ -109,4 +114,14 @@ async function getCloudAccount(state) {
|
|
|
109
114
|
if (!result.success) throw new Error("The hosted API returned invalid account data.");
|
|
110
115
|
return result.data;
|
|
111
116
|
}
|
|
112
|
-
|
|
117
|
+
async function getCloudSites(state) {
|
|
118
|
+
const result = gscdumpAvailableSiteSchema.array().safeParse(await cloudRequest(state, "/cli/sites/available"));
|
|
119
|
+
if (!result.success) throw new Error("The hosted API returned invalid Site data.");
|
|
120
|
+
return result.data;
|
|
121
|
+
}
|
|
122
|
+
function formatHostedSync(site) {
|
|
123
|
+
if (!site.registered) return void 0;
|
|
124
|
+
const status = site.syncStatus ?? "pending";
|
|
125
|
+
return `${status}${site.syncProgress && site.syncProgress.total > 0 && status !== "synced" ? `: ${site.syncProgress.completed.toLocaleString("en-US")} of ${site.syncProgress.total.toLocaleString("en-US")} days (${Math.round(site.syncProgress.percent)}%)` : ""}${status === "synced" && site.oldestDateSynced && site.newestDateSynced ? `: ${site.oldestDateSynced} to ${site.newestDateSynced}` : ""}`;
|
|
126
|
+
}
|
|
127
|
+
export { clearAuthentication, cloudRequest, formatHostedSync, getCloudAccount, getCloudSites, parseAuthMode, parseAuthentication, resolveAuthentication, saveAuthentication };
|