@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.
Files changed (73) hide show
  1. package/README.md +113 -30
  2. package/bin/gscdump.mjs +21 -1
  3. package/dist/analysis-local.mjs +172 -53
  4. package/dist/auth-state.mjs +21 -6
  5. package/dist/auth.mjs +27 -18
  6. package/dist/bing-data.mjs +6 -2
  7. package/dist/bing-hosted.mjs +1 -1
  8. package/dist/cli-args.mjs +8 -0
  9. package/dist/cli.d.mts +5 -1
  10. package/dist/cli.mjs +41 -10
  11. package/dist/cloud-google.mjs +8 -1
  12. package/dist/command-meta.mjs +4 -4
  13. package/dist/command-registry.mjs +86 -10
  14. package/dist/commands/analyze.mjs +84 -43
  15. package/dist/commands/auth.mjs +46 -37
  16. package/dist/commands/bing.mjs +3 -3
  17. package/dist/commands/compact.mjs +12 -8
  18. package/dist/commands/config.mjs +14 -14
  19. package/dist/commands/doctor.mjs +26 -26
  20. package/dist/commands/dump.mjs +203 -105
  21. package/dist/commands/entities.mjs +10 -113
  22. package/dist/commands/gc.mjs +4 -3
  23. package/dist/commands/indexing-urls.mjs +185 -0
  24. package/dist/commands/indexing.mjs +12 -8
  25. package/dist/commands/init.mjs +77 -18
  26. package/dist/commands/inspect.mjs +97 -110
  27. package/dist/commands/mcp.mjs +37 -57
  28. package/dist/commands/papercut.mjs +1 -1
  29. package/dist/commands/profile.mjs +9 -17
  30. package/dist/commands/query.mjs +219 -210
  31. package/dist/commands/report.mjs +68 -85
  32. package/dist/commands/rollups.mjs +9 -6
  33. package/dist/commands/sitemaps.mjs +51 -104
  34. package/dist/commands/sites.mjs +40 -24
  35. package/dist/commands/stats.mjs +14 -17
  36. package/dist/commands/store-purge.mjs +9 -5
  37. package/dist/commands/store.mjs +0 -12
  38. package/dist/commands/sync.mjs +774 -296
  39. package/dist/config.mjs +7 -4
  40. package/dist/context.mjs +51 -21
  41. package/dist/coverage.mjs +251 -0
  42. package/dist/dump-bing.mjs +71 -0
  43. package/dist/dump-writers.mjs +246 -0
  44. package/dist/error-handler.mjs +108 -38
  45. package/dist/filters.mjs +125 -0
  46. package/dist/hosted-site.mjs +91 -0
  47. package/dist/inspect-urls.mjs +76 -0
  48. package/dist/inspection-record.mjs +93 -0
  49. package/dist/local-entities.mjs +658 -0
  50. package/dist/mcp/errors.mjs +71 -18
  51. package/dist/mcp/handlers/diagnostics.mjs +15 -12
  52. package/dist/mcp/handlers/reports.mjs +33 -42
  53. package/dist/mcp/server/index.mjs +80 -38
  54. package/dist/mcp/types.mjs +7 -7
  55. package/dist/package.mjs +1 -1
  56. package/dist/quota-ledger.mjs +370 -0
  57. package/dist/render/analysis.mjs +8 -1
  58. package/dist/render/report.mjs +8 -1
  59. package/dist/request-pacer.mjs +29 -0
  60. package/dist/route.mjs +295 -0
  61. package/dist/sitemap.mjs +4 -0
  62. package/dist/sql-views.mjs +117 -0
  63. package/dist/store-sites.mjs +134 -0
  64. package/dist/sync-plan.mjs +129 -0
  65. package/dist/sync-run.mjs +96 -0
  66. package/dist/table-sources.mjs +57 -0
  67. package/dist/token-info.mjs +54 -0
  68. package/dist/utils.mjs +28 -19
  69. package/dist/window.mjs +159 -0
  70. package/package.json +17 -14
  71. package/skills/gscdump/SKILL.md +167 -37
  72. package/dist/commands/export.mjs +0 -76
  73. package/dist/native-duckdb.mjs +0 -46
package/dist/config.mjs CHANGED
@@ -2,8 +2,8 @@ import { useCliRuntime } from "./runtime.mjs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { z } from "zod";
5
- import os from "node:os";
6
5
  import { SearchTypes } from "gscdump/query";
6
+ import os from "node:os";
7
7
  function setConfigDir(dir) {
8
8
  useCliRuntime().configDir = dir;
9
9
  }
@@ -14,9 +14,7 @@ const configSchema = z.strictObject({
14
14
  clientId: z.string().optional(),
15
15
  clientSecret: z.string().optional(),
16
16
  defaultSite: z.string().optional(),
17
- defaultPeriod: z.string().optional(),
18
17
  defaultFormat: z.enum(["json", "csv"]).optional(),
19
- defaultDb: z.string().optional(),
20
18
  dataDir: z.string().optional(),
21
19
  defaultLimit: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional(),
22
20
  defaultSearchType: z.enum(SearchTypes).optional(),
@@ -27,7 +25,12 @@ const configSchema = z.strictObject({
27
25
  ]).optional(),
28
26
  serviceAccountPath: z.string().optional()
29
27
  });
30
- const RETIRED_KEYS = ["mode", "cloudUrl"];
28
+ const RETIRED_KEYS = [
29
+ "mode",
30
+ "cloudUrl",
31
+ "defaultPeriod",
32
+ "defaultDb"
33
+ ];
31
34
  function dropRetiredKeys(value) {
32
35
  if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
33
36
  return Object.fromEntries(Object.entries(value).filter(([key]) => !RETIRED_KEYS.includes(key)));
package/dist/context.mjs CHANGED
@@ -1,16 +1,20 @@
1
+ import { openQuotaLedger, quotaFetchOptions } from "./quota-ledger.mjs";
1
2
  import { resolveAuthentication } from "./auth-state.mjs";
2
3
  import { loadResolvedConfig } from "./config.mjs";
3
- import { logger } from "./utils.mjs";
4
4
  import { resolveAuth } from "./auth.mjs";
5
5
  import { createCloudGoogleClient } from "./cloud-google.mjs";
6
6
  import { createLocalStore } from "./local-store.mjs";
7
+ import { listStoreSites } from "./store-sites.mjs";
7
8
  import process from "node:process";
8
9
  import { cancel, isCancel, select } from "@clack/prompts";
9
10
  import { googleSearchConsole } from "gscdump/client";
11
+ import { parseGscSiteUrl, resolveSiteInput } from "gscdump";
12
+ const FULL_SITE_URL_RE = /^(?:sc-domain:|https?:\/\/)/i;
10
13
  async function createCommandContext(opts = {}) {
11
- const { needsAuth = false, needsStore = false, interactive = false, byok, fetchOptions } = opts;
14
+ const { needsAuth = false, needsStore = false, interactive = false, byok, quota = true } = opts;
12
15
  const { config, dataDir } = await loadResolvedConfig();
13
16
  const authentication = needsAuth ? await resolveAuthentication() : { _tag: "Local" };
17
+ const fetchOptions = needsAuth && quota ? quotaFetchOptions(await openQuotaLedger({ dataDir }), opts.fetchOptions) : opts.fetchOptions;
14
18
  const auth = needsAuth && authentication._tag === "Local" ? await resolveAuth({
15
19
  interactive,
16
20
  config,
@@ -25,27 +29,35 @@ async function createCommandContext(opts = {}) {
25
29
  permissionLevel: s.permissionLevel || "unknown"
26
30
  }));
27
31
  };
28
- const resolveSite = async (target) => {
29
- const hint = target ?? config.defaultSite;
30
- const sites = await loadSites();
31
- if (sites.length === 0) {
32
- logger.error("No verified sites found");
33
- process.exit(1);
34
- }
32
+ const storeCandidates = async () => (await listStoreSites(dataDir)).map((site) => ({
33
+ siteUrl: site.siteUrl,
34
+ inStore: true
35
+ }));
36
+ const asksGoogle = (options) => options.scope !== "store" && client !== null;
37
+ const matchSite = async (input, options = {}) => {
38
+ const local = await storeCandidates();
39
+ const fromStore = resolveSiteInput(input, local);
40
+ if (!asksGoogle(options) || fromStore.kind === "resolved" && (fromStore.via === "exact" || !FULL_SITE_URL_RE.test(input.trim()))) return fromStore;
41
+ return resolveSiteInput(input, [...local, ...await loadSites()]);
42
+ };
43
+ const resolveSite = async (target, options = {}) => {
44
+ const hint = target?.trim() || config.defaultSite;
45
+ const askGoogle = asksGoogle(options);
35
46
  if (hint) {
36
- const exact = sites.find((site) => site.siteUrl === hint);
37
- if (exact) return exact.siteUrl;
38
- const matches = sites.filter((site) => site.siteUrl.includes(hint));
39
- if (matches.length === 1) return matches[0].siteUrl;
40
- if (matches.length > 1) throw new Error(`Multiple Sites match "${hint}". Use an exact Site URL from gscdump sites.`);
41
- throw new Error(`No verified Site matches "${hint}". Run gscdump sites to list available Sites.`);
47
+ const resolution = await matchSite(hint, options);
48
+ if (resolution.kind === "resolved") return resolution.siteUrl;
49
+ throw new Error(formatSiteResolution(resolution, askGoogle ? "account" : "store"));
42
50
  }
43
- if (sites.length === 1) return sites[0].siteUrl;
51
+ const local = await storeCandidates();
52
+ const pool = [...new Set([...local, ...askGoogle ? await loadSites() : []].map((site) => site.siteUrl))];
53
+ if (pool.length === 0) throw new Error(askGoogle ? "No verified Sites found. Add the Site in Search Console, then run `gscdump sites`." : "The Store has no data. Run `gscdump sync --site example.com` first.");
54
+ if (pool.length === 1) return pool[0];
55
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error(`Pass --site. Sites: ${pool.join(", ")}`);
44
56
  const selected = await select({
45
- message: "Select a site",
46
- options: sites.map((s) => ({
47
- value: s.siteUrl,
48
- label: s.siteUrl
57
+ message: "Select a Site",
58
+ options: pool.map((siteUrl) => ({
59
+ value: siteUrl,
60
+ label: siteUrl
49
61
  }))
50
62
  });
51
63
  if (isCancel(selected)) {
@@ -62,7 +74,25 @@ async function createCommandContext(opts = {}) {
62
74
  client,
63
75
  store,
64
76
  loadSites,
77
+ matchSite,
65
78
  resolveSite
66
79
  };
67
80
  }
68
- export { createCommandContext };
81
+ function siteArg(siteUrl) {
82
+ return siteUrl.startsWith("http://") ? siteUrl : parseGscSiteUrl(siteUrl).displayLabel;
83
+ }
84
+ function formatSiteResolution(resolution, scope) {
85
+ switch (resolution.kind) {
86
+ case "ambiguous": return `"${resolution.input}" matches more than one Site: ${resolution.candidates.join(", ")}. Pass one of these Site URLs to --site.`;
87
+ case "covered-by-parent":
88
+ if (scope === "store") return `"${resolution.input}" is part of the Site ${resolution.parent}. The Store keeps page paths only. Pass --live to filter by host.`;
89
+ return `"${resolution.input}" is part of the Site ${resolution.parent}. Filter its pages instead: --site ${siteArg(resolution.parent)} --page ~${resolution.input.trim().replace(/^[a-z]+:\/\//i, "")}.`;
90
+ case "not-found":
91
+ if (scope === "store") {
92
+ const sync = `gscdump sync --site ${siteArg(resolution.input.trim())}`;
93
+ return resolution.known.length === 0 ? `The Store has no data. Run \`${sync}\` first.` : `The Store has no data for "${resolution.input}". Local Sites: ${resolution.known.join(", ")}. Run \`${sync}\` to add it.`;
94
+ }
95
+ return resolution.known.length === 0 ? `No Site matches "${resolution.input}". Your account has no verified Sites.` : `No Site matches "${resolution.input}". Sites: ${resolution.known.join(", ")}.`;
96
+ }
97
+ }
98
+ export { createCommandContext, formatSiteResolution, siteArg };
@@ -0,0 +1,251 @@
1
+ import { quotaLedgerPath, quotaStatus, readLedgerState } from "./quota-ledger.mjs";
2
+ import { INSPECTION_QPD_PER_PROPERTY } from "./inspection-record.mjs";
3
+ import { inspectionCandidates, loadInspectionState } from "./local-entities.mjs";
4
+ import { datesForJob, jobWindowStart } from "./sync-plan.mjs";
5
+ import { getDateRange, getLatestGscDate, getOldestGscDate, getPstDate } from "gscdump/dates";
6
+ import { createSitemapListStore, createSitemapReadStore, parseSitemapFeedIdentity } from "@gscdump/engine/entities";
7
+ function partialOrComplete(done, total, failed, extra = {}) {
8
+ if (total === 0) return { kind: "empty" };
9
+ if (done >= total && failed === 0) return {
10
+ kind: "complete",
11
+ done
12
+ };
13
+ return {
14
+ kind: "partial",
15
+ done,
16
+ total,
17
+ failed,
18
+ pending: Math.max(0, total - done - failed),
19
+ ...extra
20
+ };
21
+ }
22
+ function analyticsCoverage(input) {
23
+ const byJob = /* @__PURE__ */ new Map();
24
+ for (const state of input.states) {
25
+ const searchType = state.searchType ?? "web";
26
+ const key = `${state.table}|${searchType}`;
27
+ const job = byJob.get(key) ?? {
28
+ table: state.table,
29
+ searchType,
30
+ states: []
31
+ };
32
+ job.states.push({
33
+ date: state.date,
34
+ state: state.state
35
+ });
36
+ byJob.set(key, job);
37
+ }
38
+ const jobs = [];
39
+ const dayState = /* @__PURE__ */ new Map();
40
+ const window = {
41
+ kind: "catch-up",
42
+ firstStart: input.latest,
43
+ floor: input.floor,
44
+ end: input.latest
45
+ };
46
+ for (const job of byJob.values()) {
47
+ const from = jobWindowStart(window, job.states);
48
+ const dates = datesForJob(job.table, getDateRange(from, input.latest), input.today);
49
+ const stateByDate = new Map(job.states.map((state) => [state.date, state.state]));
50
+ let done = 0;
51
+ let failed = 0;
52
+ for (const date of dates) {
53
+ const state = stateByDate.get(date);
54
+ const prior = dayState.get(date);
55
+ if (state === "done") {
56
+ done++;
57
+ if (!prior) dayState.set(date, "done");
58
+ } else if (state === "failed") {
59
+ failed++;
60
+ dayState.set(date, "failed");
61
+ } else if (prior !== "failed") dayState.set(date, "pending");
62
+ }
63
+ jobs.push({
64
+ table: job.table,
65
+ searchType: job.searchType,
66
+ from: dates[0] ?? from,
67
+ to: input.latest,
68
+ coverage: partialOrComplete(done, dates.length, failed)
69
+ });
70
+ }
71
+ jobs.sort((a, b) => a.table.localeCompare(b.table) || a.searchType.localeCompare(b.searchType));
72
+ const sortedDays = [...dayState.keys()].sort();
73
+ const values = [...dayState.values()];
74
+ return {
75
+ jobs,
76
+ days: {
77
+ from: sortedDays[0] ?? input.latest,
78
+ to: input.latest,
79
+ coverage: partialOrComplete(values.filter((v) => v === "done").length, values.length, values.filter((v) => v === "failed").length)
80
+ }
81
+ };
82
+ }
83
+ function inspectionCoverage(input) {
84
+ const { inspected } = input;
85
+ const total = new Set(input.candidates).size;
86
+ const done = [...new Set(input.candidates)].filter((url) => inspected.has(url)).length;
87
+ const pending = total - done;
88
+ const perRun = Math.max(0, Math.min(input.perRun, INSPECTION_QPD_PER_PROPERTY));
89
+ const etaRuns = perRun > 0 ? Math.ceil(pending / perRun) : void 0;
90
+ return {
91
+ perRun,
92
+ coverage: partialOrComplete(done, total, 0, {
93
+ ...etaRuns !== void 0 ? {
94
+ etaRuns,
95
+ etaDays: etaRuns
96
+ } : {},
97
+ ...input.blockedUntil !== void 0 ? { resumesAt: input.blockedUntil } : {}
98
+ })
99
+ };
100
+ }
101
+ function datasetKey(table, searchType) {
102
+ return `${table}|${searchType ?? "web"}`;
103
+ }
104
+ function syncStateCoverageReader(states) {
105
+ const done = /* @__PURE__ */ new Map();
106
+ for (const state of states) {
107
+ if (state.state !== "done") continue;
108
+ const key = datasetKey(state.table, state.searchType);
109
+ done.set(key, [...done.get(key) ?? [], state.date]);
110
+ }
111
+ return { datesForTable: async ({ table, searchType, start, end }) => (done.get(datasetKey(table, searchType)) ?? []).filter((date) => date >= start && date <= end) };
112
+ }
113
+ function hasSyncedDays(states, table, searchType) {
114
+ const key = datasetKey(table, searchType);
115
+ return states.some((state) => state.state === "done" && datasetKey(state.table, state.searchType) === key);
116
+ }
117
+ function n(value) {
118
+ return value.toLocaleString("en-US");
119
+ }
120
+ function plural(count, one, many) {
121
+ return `${n(count)} ${count === 1 ? one : many}`;
122
+ }
123
+ function clock(at) {
124
+ return new Date(at).toLocaleString("en-US", {
125
+ hour: "numeric",
126
+ minute: "2-digit",
127
+ month: "short",
128
+ day: "numeric"
129
+ });
130
+ }
131
+ function nextCommand(coverage) {
132
+ if (coverage.run.kind === "running") return `gscdump sync --status --site ${coverage.site}`;
133
+ return `gscdump sync --site ${coverage.site}`;
134
+ }
135
+ function analyticsLine(coverage) {
136
+ const { from, to, coverage: c } = coverage.days;
137
+ if (c.kind === "empty") return `Analytics: nothing synced yet. A first sync fetches the last 28 days.`;
138
+ if (c.kind === "complete") return `Analytics: all ${plural(c.done, "day", "days")} from ${from} to ${to}, every table.`;
139
+ const failed = c.failed > 0 ? ` ${plural(c.failed, "day has", "days have")} a failed table.` : "";
140
+ const next = coverage.analyticsBlockedUntil !== void 0 ? ` Google paused Search Analytics calls until ${clock(coverage.analyticsBlockedUntil)}. The next sync after that continues.` : " The next sync continues from there.";
141
+ return `Analytics: ${n(c.done)} of ${plural(c.total, "day", "days")} so far (${from} to ${to}).${failed}${next}`;
142
+ }
143
+ function inspectionLine(coverage) {
144
+ const { coverage: c, perRun } = coverage.inspections;
145
+ if (c.kind === "empty") return "Inspections: no URLs yet. Sync saves sitemaps and pages first.";
146
+ if (c.kind === "complete") return `Inspections: all ${plural(c.done, "URL", "URLs")} inspected at least once.`;
147
+ const head = `Inspections: ${n(c.done)} of ${plural(c.total, "URL", "URLs")} so far.`;
148
+ const blocked = c.resumesAt !== void 0 ? ` Google allows more after ${clock(c.resumesAt)}.` : "";
149
+ if (perRun === 0) return `${head}${blocked} Pass --inspect-limit to inspect URLs during sync.`;
150
+ const eta = c.etaRuns ?? 0;
151
+ if (perRun >= 2e3) return `${head}${blocked} Daily sync covers the rest in about ${plural(eta, "day", "days")} at ${n(perRun)} URLs a day.`;
152
+ const fastest = Math.ceil(c.pending / INSPECTION_QPD_PER_PROPERTY);
153
+ const raise = fastest < eta ? ` Pass --inspect-limit ${n(INSPECTION_QPD_PER_PROPERTY).replace(",", "")} to finish in about ${plural(fastest, "day", "days")}.` : "";
154
+ return `${head}${blocked} Daily sync covers the rest in about ${plural(eta, "run", "runs")} at ${n(perRun)} URLs a run.${raise}`;
155
+ }
156
+ function sitemapLine(coverage) {
157
+ const { coverage: c, urls } = coverage.sitemaps;
158
+ if (c.kind === "empty") return "Sitemaps: none saved yet.";
159
+ if (c.kind === "complete") return `Sitemaps: all ${plural(c.done, "sitemap", "sitemaps")} saved, ${plural(urls, "URL", "URLs")}.`;
160
+ return `Sitemaps: ${n(c.done)} of ${plural(c.total, "sitemap", "sitemaps")} saved so far, ${plural(urls, "URL", "URLs")}. The next sync reads the rest.`;
161
+ }
162
+ function renderCoverage(coverage) {
163
+ const lines = [
164
+ analyticsLine(coverage),
165
+ inspectionLine(coverage),
166
+ sitemapLine(coverage)
167
+ ];
168
+ if (coverage.run.kind === "running") {
169
+ const { record } = coverage.run;
170
+ lines.unshift(`Sync running: ${n(record.done)}/${n(record.planned)} days, pid ${record.pid}.`);
171
+ } else if (coverage.run.kind === "stale") {
172
+ const { record } = coverage.run;
173
+ lines.unshift(`The last sync stopped at ${n(record.done)}/${n(record.planned)} days without finishing (pid ${record.pid}). The next sync retries its dates.`);
174
+ }
175
+ lines.push(`Next: ${nextCommand(coverage)}`);
176
+ return lines;
177
+ }
178
+ async function sitemapCoverage(store, siteUrl) {
179
+ const ctx = {
180
+ userId: store.userId,
181
+ siteId: store.siteIdFor(siteUrl)
182
+ };
183
+ const list = await createSitemapListStore({ dataSource: store.dataSource }).load(ctx);
184
+ if (!list) return {
185
+ coverage: { kind: "empty" },
186
+ urls: 0
187
+ };
188
+ const feeds = /* @__PURE__ */ new Set();
189
+ let urls = 0;
190
+ const iterated = await createSitemapReadStore({ dataSource: store.dataSource }).iterateSitemapGenerationUrls(ctx);
191
+ if (iterated._tag === "iterator") for await (const record of iterated.items) {
192
+ feeds.add(record.feedpath);
193
+ urls++;
194
+ }
195
+ const expected = list.sitemaps.filter((sitemap) => sitemap.contents.some((content) => content.submitted > 0));
196
+ const saved = expected.filter((sitemap) => {
197
+ const identity = parseSitemapFeedIdentity(sitemap.path);
198
+ return identity._tag === "ok" && feeds.has(identity.url);
199
+ }).length;
200
+ if (list.sitemaps.length === 0) return {
201
+ coverage: {
202
+ kind: "complete",
203
+ done: 0
204
+ },
205
+ urls
206
+ };
207
+ return {
208
+ coverage: partialOrComplete(saved, expected.length, 0),
209
+ urls
210
+ };
211
+ }
212
+ async function readStoreCoverage(input) {
213
+ const { store, site } = input;
214
+ const now = input.now ?? /* @__PURE__ */ new Date();
215
+ const ctx = {
216
+ userId: store.userId,
217
+ siteId: store.siteIdFor(site)
218
+ };
219
+ const ledger = input.ledger ?? await readLedgerState(quotaLedgerPath(store.dataDir));
220
+ const { jobs: analytics, days } = analyticsCoverage({
221
+ states: await store.engine.getSyncStates(ctx),
222
+ latest: getLatestGscDate(),
223
+ floor: getOldestGscDate(),
224
+ today: getPstDate(now)
225
+ });
226
+ const inspectBlock = quotaStatus(ledger, {
227
+ api: "urlInspection",
228
+ site,
229
+ now
230
+ }).blocked;
231
+ const analyticsBlock = quotaStatus(ledger, {
232
+ api: "searchAnalytics",
233
+ site,
234
+ now
235
+ }).blocked;
236
+ return {
237
+ site,
238
+ analytics,
239
+ days,
240
+ inspections: inspectionCoverage({
241
+ candidates: await inspectionCandidates(store, site),
242
+ inspected: new Set((await loadInspectionState(store.dataSource, ctx, now)).latest.keys()),
243
+ perRun: input.inspectLimit,
244
+ ...inspectBlock ? { blockedUntil: inspectBlock.until } : {}
245
+ }),
246
+ sitemaps: await sitemapCoverage(store, site),
247
+ ...analyticsBlock ? { analyticsBlockedUntil: analyticsBlock.until } : {},
248
+ run: input.run
249
+ };
250
+ }
251
+ export { analyticsCoverage, hasSyncedDays, inspectionCoverage, nextCommand, readStoreCoverage, renderCoverage, syncStateCoverageReader };
@@ -0,0 +1,71 @@
1
+ import { resolveAuthentication } from "./auth-state.mjs";
2
+ import { urlInProperty } from "./local-entities.mjs";
3
+ import { getBingClient, inspectBingCredentials } from "./bing-auth.mjs";
4
+ import { dumpBingSite, parseBingDumpOptions, unwrapBing } from "./bing-data.mjs";
5
+ import { dumpHostedBingSite, listHostedBingSites } from "./bing-hosted.mjs";
6
+ import fs from "node:fs/promises";
7
+ import path from "node:path";
8
+ function matchBingSites(bingSites, googleSites) {
9
+ if (googleSites === "all") return [...bingSites];
10
+ return bingSites.filter((bing) => googleSites.some((google) => urlInProperty(google, bing) || urlInProperty(bing, google)));
11
+ }
12
+ async function withSizes(summary) {
13
+ const files = await Promise.all(summary.files.map(async (file) => ({
14
+ dataset: file.dataset,
15
+ path: file.path,
16
+ bytes: (await fs.stat(file.path)).size,
17
+ rows: file.rows
18
+ })));
19
+ return {
20
+ siteUrl: summary.siteUrl,
21
+ files
22
+ };
23
+ }
24
+ function message(error) {
25
+ return error instanceof Error ? error.message : String(error);
26
+ }
27
+ async function dumpBing(opts) {
28
+ const options = parseBingDumpOptions({ format: opts.format === "csv" || opts.format === "ndjson" ? opts.format : "json" });
29
+ const bingDir = path.join(opts.outDir, "bing");
30
+ const sites = [];
31
+ const failures = [];
32
+ const authentication = await resolveAuthentication();
33
+ if (authentication._tag === "Cloud") {
34
+ const hosted = (await listHostedBingSites(authentication)).filter((site) => site.connection._tag === "connected");
35
+ const wanted = new Set(matchBingSites(hosted.map((site) => site.siteUrl), opts.googleSites));
36
+ const selected = hosted.filter((site) => wanted.has(site.siteUrl));
37
+ if (selected.length === 0) return {
38
+ _tag: "skipped",
39
+ reason: "No connected Bing site matches the dumped sites."
40
+ };
41
+ for (const site of selected) await dumpHostedBingSite(authentication, site, bingDir, options).then(async (summary) => sites.push(await withSizes(summary))).catch((error) => failures.push({
42
+ siteUrl: site.siteUrl,
43
+ error: message(error)
44
+ }));
45
+ return {
46
+ _tag: "dumped",
47
+ sites,
48
+ failures
49
+ };
50
+ }
51
+ if ((await inspectBingCredentials())._tag === "Missing") return {
52
+ _tag: "skipped",
53
+ reason: "No Bing login. Run `gscdump bing login` to include Bing."
54
+ };
55
+ const client = await getBingClient();
56
+ const selected = matchBingSites(unwrapBing(await client.getUserSites({ signal: AbortSignal.timeout(3e4) })).filter((site) => site.isVerified).map((site) => site.url), opts.googleSites);
57
+ if (selected.length === 0) return {
58
+ _tag: "skipped",
59
+ reason: "No verified Bing site matches the dumped sites."
60
+ };
61
+ for (const siteUrl of selected) await dumpBingSite(client, siteUrl, bingDir, options).then(async (summary) => sites.push(await withSizes(summary))).catch((error) => failures.push({
62
+ siteUrl,
63
+ error: message(error)
64
+ }));
65
+ return {
66
+ _tag: "dumped",
67
+ sites,
68
+ failures
69
+ };
70
+ }
71
+ export { dumpBing, matchBingSites };