@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
@@ -1,25 +1,18 @@
1
- import { useCliRuntime } from "../runtime.mjs";
2
- import { clearAuthentication, getCloudAccount, parseAuthMode, parseAuthentication, resolveAuthentication, saveAuthentication } from "../auth-state.mjs";
3
1
  import { authCommandMeta } from "../command-meta.mjs";
2
+ import { useCliRuntime } from "../runtime.mjs";
3
+ import { clearAuthentication, formatHostedSync, getCloudAccount, getCloudSites, parseAuthMode, parseAuthentication, resolveAuthentication, saveAuthentication } from "../auth-state.mjs";
4
4
  import { loadConfig, saveConfig } from "../config.mjs";
5
- import { OUTPUT_ARGS, applyOutputMode, logger, noSubcommandSelected } from "../utils.mjs";
6
- import { clearTokens, formatAuthProvenance, getAuth, loadServiceAccount, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
5
+ import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
6
+ import { GOOGLE_NOT_CONNECTED, clearTokens, formatAuthProvenance, getAuth, loadServiceAccount, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
7
7
  import { missingRequiredScopes } from "../auth-scopes.mjs";
8
8
  import { clearBingCredentials, getBingClient, inspectBingCredentials } from "../bing-auth.mjs";
9
+ import { currentAccessToken, fetchTokenInfo, redactTokens } from "../token-info.mjs";
9
10
  import { runSmokeTest } from "./init.mjs";
10
11
  import { adoptCurrentConfigAsProfile, profileNameFromEmail, resolveActiveProfile } from "./profile.mjs";
11
12
  import process from "node:process";
12
13
  import { defineCommand } from "citty";
13
14
  import path from "node:path";
14
15
  import { isCancel, password } from "@clack/prompts";
15
- import { ofetch } from "ofetch";
16
- const AUTH_SUBCOMMANDS = [
17
- "status",
18
- "login",
19
- "logout",
20
- "refresh",
21
- "scopes"
22
- ];
23
16
  const MODE_ARG = {
24
17
  type: "string",
25
18
  description: "Authentication mode: cloud or local"
@@ -52,24 +45,26 @@ async function loginCloud(args) {
52
45
  logger.success(`Cloud authentication saved for ${account.user.email}`);
53
46
  logger.info("Google and Bing commands use connections saved on gscdump.com.");
54
47
  }
55
- async function fetchTokenInfo(accessToken) {
56
- return ofetch("https://oauth2.googleapis.com/tokeninfo", { query: { access_token: accessToken } }).catch(() => null);
57
- }
58
48
  async function resolveLiveAuthState() {
59
49
  const tokens = await loadTokens();
60
50
  const byok = resolveBYOK();
61
- let liveToken = null;
62
- if (typeof byok === "string") liveToken = byok;
63
- else if (byok && "getAccessToken" in byok) liveToken = await byok.getAccessToken().then((r) => r.token ?? null).catch(() => null);
64
- else if (tokens?.access_token) liveToken = tokens.access_token;
65
- const tokenInfo = liveToken ? await fetchTokenInfo(liveToken) : null;
51
+ const source = byok ?? (tokens ? await getAuth({ interactive: false }).catch((error) => error instanceof Error ? error : new Error(String(error))) : null);
52
+ const current = source === null ? null : source instanceof Error ? {
53
+ kind: "failed",
54
+ detail: redactTokens(source.message)
55
+ } : await currentAccessToken(source);
56
+ const liveToken = current?.kind === "token" ? current.token : null;
57
+ const verified = liveToken ? await fetchTokenInfo(liveToken) : null;
58
+ const tokenInfo = verified?.kind === "valid" ? verified.info : null;
59
+ const failure = current?.kind === "failed" ? current.detail : verified?.kind === "invalid" ? verified.detail : null;
66
60
  const scopes = tokenInfo?.scope ? tokenInfo.scope.split(/\s+/).filter(Boolean) : [];
67
61
  const missing = missingRequiredScopes(scopes, byok ? void 0 : tokens?.provider);
68
62
  return {
69
63
  byok,
70
- tokens,
64
+ tokens: tokens && !byok ? await loadTokens() : tokens,
71
65
  liveToken,
72
66
  tokenInfo,
67
+ failure,
73
68
  scopes,
74
69
  missing
75
70
  };
@@ -127,25 +122,42 @@ async function runStatus(args) {
127
122
  }
128
123
  return;
129
124
  }
125
+ const hosted = await getCloudSites(authentication).then((sites) => ({
126
+ _tag: "Ok",
127
+ sites: sites.filter((site) => site.registered)
128
+ }), (error) => ({
129
+ _tag: "Err",
130
+ detail: error instanceof Error ? error.message : "Hosted Site list failed."
131
+ }));
130
132
  if (json) console.log(JSON.stringify({
131
133
  authenticated: true,
132
134
  mode: "cloud",
133
135
  account: account.value.user.email,
134
136
  apiRoot: authentication.apiRoot,
135
137
  sites: account.value.sites,
138
+ ...hosted._tag === "Ok" ? { hostedSync: hosted.sites.map((site) => ({
139
+ siteUrl: site.siteUrl,
140
+ syncStatus: site.syncStatus ?? null,
141
+ syncProgress: site.syncProgress ?? null,
142
+ oldestDateSynced: site.oldestDateSynced ?? null,
143
+ newestDateSynced: site.newestDateSynced ?? null
144
+ })) } : { hostedSyncError: hosted.detail },
136
145
  capabilities
137
146
  }, null, 2));
138
147
  else {
139
148
  logger.success(`Authenticated with cloud: ${account.value.user.email}`);
140
149
  console.log(` API: ${authentication.apiRoot}`);
141
150
  console.log(` Sites: ${account.value.sites.length}`);
151
+ if (hosted._tag === "Ok") for (const site of hosted.sites) console.log(` ${site.siteUrl} ${formatHostedSync(site)}`);
152
+ else console.log(` Hosted sync status is not available: ${hosted.detail}`);
142
153
  console.log(" Google and Bing use connections saved on gscdump.com.");
143
154
  console.log(` Cloud commands: ${capabilities.cloud.join(", ")}`);
144
155
  console.log(" Google indexing and Site Verification require --mode local.");
145
156
  }
146
157
  return;
147
158
  }
148
- const { byok, tokens, tokenInfo, scopes, missing } = await resolveLiveAuthState();
159
+ const { byok, tokens, tokenInfo, failure, scopes, missing } = await resolveLiveAuthState();
160
+ const googleAuthenticated = tokenInfo !== null;
149
161
  const bingCredentials = await inspectBingCredentials();
150
162
  const bingResult = bingCredentials._tag === "Missing" ? null : await getBingClient().then((client) => client.getUserSites({ signal: AbortSignal.timeout(1e4) })).catch(() => null);
151
163
  const bing = {
@@ -156,9 +168,10 @@ async function runStatus(args) {
156
168
  const byokKind = byok ? typeof byok === "string" ? "access-token" : "refresh-token" : null;
157
169
  if (json) {
158
170
  console.log(JSON.stringify({
159
- authenticated: !!tokens || !!byok || bing.authenticated,
171
+ authenticated: googleAuthenticated || bing.authenticated,
160
172
  mode: "local",
161
- googleAuthenticated: !!tokens || !!byok,
173
+ googleAuthenticated,
174
+ googleError: failure,
162
175
  bing,
163
176
  source: byok ? "byok" : tokens ? "saved-tokens" : null,
164
177
  byokKind,
@@ -186,7 +199,8 @@ async function runStatus(args) {
186
199
  }
187
200
  };
188
201
  if (byok) {
189
- logger.success(`Authenticated via BYOK (${byokKind})`);
202
+ if (googleAuthenticated) logger.success(`Authenticated via BYOK (${byokKind})`);
203
+ else logger.warn(`BYOK credentials (${byokKind}) failed verification: ${failure}`);
190
204
  if (tokenInfo?.email) console.log(` Account: ${tokenInfo.email}`);
191
205
  reportScopes();
192
206
  return;
@@ -196,18 +210,15 @@ async function runStatus(args) {
196
210
  logger.info("Google credentials are missing. Run `gscdump auth login --mode local` to connect Google.");
197
211
  return;
198
212
  }
199
- logger.warn("Not authenticated");
200
- logger.info("Run `gscdump init` (full setup) or `gscdump auth login` (OAuth only)");
201
- logger.info("Or set GSC_ACCESS_TOKEN / GSC_CLIENT_ID + GSC_CLIENT_SECRET + GSC_REFRESH_TOKEN env vars");
213
+ logger.warn(GOOGLE_NOT_CONNECTED);
202
214
  return;
203
215
  }
204
216
  const hasAccess = !!tokens.access_token;
205
217
  const hasRefresh = !!tokens.refresh_token;
206
218
  const expiry = tokens.expiry_date ? new Date(tokens.expiry_date) : null;
207
219
  const isExpired = expiry && expiry < /* @__PURE__ */ new Date();
208
- if (isExpired && hasRefresh) logger.warn("Token expired; refresh available. Run `gscdump auth refresh`, or any live command will auto-refresh.");
209
- else if (isExpired) logger.error("Token expired and no refresh token present. Run `gscdump auth login`.");
210
- else logger.success("Authenticated (saved tokens)");
220
+ if (googleAuthenticated) logger.success("Authenticated (saved tokens)");
221
+ else logger.warn(`Saved credentials failed verification: ${failure}. Run \`gscdump auth login\` to sign in again.`);
211
222
  console.log();
212
223
  console.log(` Access token: ${hasAccess ? "\x1B[32mpresent\x1B[0m" : "\x1B[31mmissing\x1B[0m"}`);
213
224
  console.log(` Refresh token: ${hasRefresh ? "\x1B[32mpresent\x1B[0m" : "\x1B[31mmissing\x1B[0m"}`);
@@ -217,7 +228,6 @@ async function runStatus(args) {
217
228
  }
218
229
  if (tokenInfo?.email) console.log(` Account: ${tokenInfo.email}`);
219
230
  reportScopes();
220
- if (isExpired && !hasRefresh) process.exit(1);
221
231
  }
222
232
  const statusCommand = defineCommand({
223
233
  meta: authCommandMeta.status,
@@ -334,9 +344,9 @@ const loginCommand = defineCommand({
334
344
  await saveAuthentication({ _tag: "Local" });
335
345
  if (!resolveActiveProfile()) {
336
346
  const tokens = await loadTokens();
337
- const info = tokens?.access_token ? await fetchTokenInfo(tokens.access_token) : null;
338
- if (info?.email) {
339
- const name = profileNameFromEmail(info.email);
347
+ const verified = tokens?.access_token ? await fetchTokenInfo(tokens.access_token) : null;
348
+ if (verified?.kind === "valid" && verified.info.email) {
349
+ const name = profileNameFromEmail(verified.info.email);
340
350
  if (await adoptCurrentConfigAsProfile(name).catch((error) => {
341
351
  const message = error instanceof Error ? error.message : String(error);
342
352
  logger.warn(`Login succeeded, but the config could not be moved into profile "${name}": ${message}`);
@@ -409,8 +419,7 @@ const authCommand = defineCommand({
409
419
  scopes: scopesCommand
410
420
  },
411
421
  async run({ args }) {
412
- if (!noSubcommandSelected("auth", AUTH_SUBCOMMANDS)) return;
413
422
  await runStatus(args);
414
423
  }
415
424
  });
416
- export { authCommand, loginCommand, logoutCommand, statusCommand };
425
+ export { authCommand, loginCloud, loginCommand, logoutCommand, statusCommand };
@@ -1,6 +1,6 @@
1
+ import { bingCommandMeta } from "../command-meta.mjs";
1
2
  import { useCliRuntime } from "../runtime.mjs";
2
3
  import { resolveAuthentication, saveAuthentication } from "../auth-state.mjs";
3
- import { bingCommandMeta } from "../command-meta.mjs";
4
4
  import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
5
5
  import { BING_REDIRECT_URI, clearBingCredentials, getBingClient, loginBingOAuth, saveBingCredentials } from "../bing-auth.mjs";
6
6
  import { BING_DATASETS, dumpBingSite, parseBingDumpOptions, resolveBingSites, unwrapBing } from "../bing-data.mjs";
@@ -223,7 +223,7 @@ const bingCommand = defineCommand({
223
223
  },
224
224
  "datasets": {
225
225
  type: "string",
226
- description: `Comma-separated datasets: ${BING_DATASETS.join(", ")}. Default: all except crawl-issues`
226
+ description: `Comma-separated datasets: ${BING_DATASETS.join(", ")}. Default: all. Crawl issues have no dates, so --start, --end, and cloud mode leave them out`
227
227
  },
228
228
  "start": {
229
229
  type: "string",
@@ -240,7 +240,7 @@ const bingCommand = defineCommand({
240
240
  const options = parseBingDumpOptions(args);
241
241
  const authentication = await resolveAuthentication();
242
242
  if (authentication._tag === "Cloud") {
243
- if (options.datasets.includes("crawl-issues")) throw new Error("Bing crawl issues require local authentication. Use --mode local.");
243
+ if (options.datasetsExplicit && options.datasets.includes("crawl-issues")) throw new Error("Bing crawl issues require local authentication. Use --mode local.");
244
244
  if (options.start && options.end && Date.parse(options.end) - Date.parse(options.start) > 316224e5) throw new Error("Hosted Bing exports support at most 366 days. Use a shorter --start and --end range.");
245
245
  const sites = await resolveHostedBingSites(authentication, {
246
246
  site: args.site,
@@ -1,5 +1,6 @@
1
1
  import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
2
2
  import { allTables } from "../local-store.mjs";
3
+ import { readSiteMap, siteUrlForId } from "../store-sites.mjs";
3
4
  import { createCommandContext } from "../context.mjs";
4
5
  import { defineCommand } from "citty";
5
6
  import { inferLegacyTier } from "@gscdump/engine";
@@ -12,7 +13,7 @@ const compactCommand = defineCommand({
12
13
  "site": {
13
14
  type: "string",
14
15
  alias: "s",
15
- description: "Restrict to a single site (default: all sites with local data)"
16
+ description: "Restrict to one Site, for example example.com (default: every Site with local data)"
16
17
  },
17
18
  "raw-days": {
18
19
  type: "string",
@@ -35,8 +36,9 @@ const compactCommand = defineCommand({
35
36
  },
36
37
  async run({ args }) {
37
38
  const { json } = applyOutputMode(args);
38
- const store = (await createCommandContext({ needsStore: true })).store;
39
- const siteId = args.site ? store.siteIdFor(String(args.site)) : void 0;
39
+ const ctx = await createCommandContext({ needsStore: true });
40
+ const store = ctx.store;
41
+ const siteId = args.site ? store.siteIdFor(await ctx.resolveSite(String(args.site), { scope: "store" })) : void 0;
40
42
  const dryRun = Boolean(args["dry-run"]);
41
43
  const thresholds = {};
42
44
  if (args["raw-days"]) thresholds.raw = Number(args["raw-days"]);
@@ -46,13 +48,15 @@ const compactCommand = defineCommand({
46
48
  userId: store.userId,
47
49
  siteId
48
50
  });
51
+ const siteMap = await readSiteMap(store.dataDir, store.userId);
52
+ const siteLabel = (id) => id ? siteUrlForId(siteMap, id) : "-";
49
53
  if (dryRun) {
50
54
  const report = [];
51
55
  for (const table of allTables()) {
52
56
  const bySite = groupBySite(liveEntries.filter((entry) => entry.table === table));
53
57
  for (const [s, group] of bySite) report.push({
54
58
  table,
55
- siteId: s,
59
+ siteUrl: s ? siteUrlForId(siteMap, s) : null,
56
60
  ...countByTier(group)
57
61
  });
58
62
  }
@@ -64,8 +68,8 @@ const compactCommand = defineCommand({
64
68
  return;
65
69
  }
66
70
  console.log();
67
- console.log(` table site raw d7 d30 d90`);
68
- for (const r of report) console.log(` ${r.table.padEnd(20)} ${(r.siteId ?? "-").padEnd(20)} ${String(r.raw).padStart(4)} ${String(r.d7).padStart(4)} ${String(r.d30).padStart(4)} ${String(r.d90).padStart(4)}`);
71
+ console.log(` table Site raw d7 d30 d90`);
72
+ for (const r of report) console.log(` ${r.table.padEnd(20)} ${(r.siteUrl ?? "-").padEnd(30)} ${String(r.raw).padStart(4)} ${String(r.d7).padStart(4)} ${String(r.d30).padStart(4)} ${String(r.d90).padStart(4)}`);
69
73
  console.log();
70
74
  logger.info(`compact --dry-run: ${report.length} (table, site) pair(s) — pass without --dry-run to apply`);
71
75
  return;
@@ -75,7 +79,7 @@ const compactCommand = defineCommand({
75
79
  const entries = liveEntries.filter((entry) => entry.table === table);
76
80
  const siteIds = new Set(entries.map((e) => e.siteId));
77
81
  for (const targetSite of siteIds) {
78
- logger.info(`Compacting ${table} [${targetSite ?? "-"}] (raw→d7→d30→d90)`);
82
+ logger.info(`Compacting ${table} [${siteLabel(targetSite)}] (raw→d7→d30→d90)`);
79
83
  await store.engine.compactTiered({
80
84
  userId: store.userId,
81
85
  siteId: targetSite,
@@ -83,7 +87,7 @@ const compactCommand = defineCommand({
83
87
  }, thresholds);
84
88
  summary.push({
85
89
  table,
86
- siteId: targetSite
90
+ siteUrl: targetSite ? siteUrlForId(siteMap, targetSite) : null
87
91
  });
88
92
  }
89
93
  }
@@ -1,11 +1,12 @@
1
1
  import { configCommandMeta } from "../command-meta.mjs";
2
2
  import { getConfigPath, loadConfig, resolveDataDir, saveConfig } from "../config.mjs";
3
- import { OUTPUT_ARGS, applyOutputMode, displayPath, logger, noSubcommandSelected } from "../utils.mjs";
4
- import { createCommandContext } from "../context.mjs";
3
+ import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs";
4
+ import { createCommandContext, formatSiteResolution } from "../context.mjs";
5
5
  import process from "node:process";
6
6
  import { defineCommand } from "citty";
7
7
  import fs from "node:fs/promises";
8
8
  import path from "node:path";
9
+ import { resolveSiteInput } from "gscdump";
9
10
  const showCommand = defineCommand({
10
11
  meta: {
11
12
  name: "show",
@@ -35,9 +36,7 @@ const showCommand = defineCommand({
35
36
  });
36
37
  const VALID_KEYS = [
37
38
  "defaultSite",
38
- "defaultPeriod",
39
39
  "defaultFormat",
40
- "defaultDb",
41
40
  "dataDir",
42
41
  "defaultLimit",
43
42
  "defaultSearchType",
@@ -45,6 +44,13 @@ const VALID_KEYS = [
45
44
  "serviceAccountPath"
46
45
  ];
47
46
  const NUMERIC_KEYS = /* @__PURE__ */ new Set(["defaultLimit"]);
47
+ async function canonicalSite(input) {
48
+ const fromStore = await (await createCommandContext()).matchSite(input, { scope: "store" });
49
+ if (fromStore.kind === "resolved") return fromStore.siteUrl;
50
+ return (await createCommandContext({ needsAuth: true }).catch((error) => {
51
+ throw new Error(`${formatSiteResolution(fromStore, "store")} ${error.message}`);
52
+ })).resolveSite(input);
53
+ }
48
54
  const setCommand = defineCommand({
49
55
  meta: {
50
56
  name: "set",
@@ -71,7 +77,7 @@ const setCommand = defineCommand({
71
77
  process.exit(1);
72
78
  }
73
79
  const config = await loadConfig();
74
- const value = NUMERIC_KEYS.has(args.key) ? Number(args.value) : args.value;
80
+ const value = NUMERIC_KEYS.has(args.key) ? Number(args.value) : args.key === "defaultSite" ? await canonicalSite(args.value) : args.value;
75
81
  if (NUMERIC_KEYS.has(args.key) && !Number.isFinite(value)) {
76
82
  logger.error(`Invalid numeric value for ${args.key}: ${args.value}`);
77
83
  process.exit(1);
@@ -163,10 +169,11 @@ const validateCommand = defineCommand({
163
169
  message: `Could not verify Site: ${message}`
164
170
  });
165
171
  });
166
- if (sites && !sites.some((s) => s.siteUrl === config.defaultSite || s.siteUrl.includes(String(config.defaultSite)))) issues.push({
172
+ const resolution = sites && resolveSiteInput(config.defaultSite, sites);
173
+ if (resolution && resolution.kind !== "resolved") issues.push({
167
174
  key: "defaultSite",
168
175
  level: "fail",
169
- message: `${config.defaultSite} is not in the verified site list`
176
+ message: formatSiteResolution(resolution, "account")
170
177
  });
171
178
  } else issues.push({
172
179
  key: "defaultSite",
@@ -212,13 +219,6 @@ const configCommand = defineCommand({
212
219
  validate: validateCommand
213
220
  },
214
221
  async run({ args }) {
215
- if (!noSubcommandSelected("config", [
216
- "show",
217
- "set",
218
- "unset",
219
- "path",
220
- "validate"
221
- ])) return;
222
222
  await showCommand.run?.({
223
223
  args,
224
224
  cmd: showCommand,
@@ -1,18 +1,20 @@
1
- import { getCloudAccount, resolveAuthentication } from "../auth-state.mjs";
2
1
  import { doctorCommandMeta } from "../command-meta.mjs";
2
+ import { getCloudAccount, resolveAuthentication } from "../auth-state.mjs";
3
3
  import { loadConfig } from "../config.mjs";
4
4
  import { resolveCliEnvironment } from "../environment.mjs";
5
5
  import { parseEnvFile } from "../env-file.mjs";
6
6
  import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs";
7
- import { loadTokens, resolveAuth, resolveBYOK } from "../auth.mjs";
7
+ import { getAuth, loadTokens, resolveAuth, resolveBYOK } from "../auth.mjs";
8
8
  import { createLocalStore } from "../local-store.mjs";
9
- import { createCommandContext } from "../context.mjs";
9
+ import { createCommandContext, formatSiteResolution } from "../context.mjs";
10
10
  import { missingRequiredScopes } from "../auth-scopes.mjs";
11
+ import { currentAccessToken, fetchTokenInfo, redactTokens } from "../token-info.mjs";
11
12
  import process from "node:process";
12
13
  import { defineCommand } from "citty";
13
14
  import fs from "node:fs/promises";
14
15
  import path from "node:path";
15
16
  import { googleSearchConsole } from "gscdump/client";
17
+ import { resolveSiteInput } from "gscdump";
16
18
  import { ofetch } from "ofetch";
17
19
  const FETCH_TIMEOUT_MS = 5e3;
18
20
  const TIME_SKEW_WARN_MS = 3e5;
@@ -97,39 +99,36 @@ async function checkAuth(envKeys) {
97
99
  status: "info",
98
100
  detail: clientId
99
101
  });
100
- let liveToken = null;
101
- let refreshError = null;
102
- if (typeof byok === "string") liveToken = byok;
103
- else if (byok && "getAccessToken" in byok) liveToken = await byok.getAccessToken().then((r) => r.token ?? null).catch((e) => {
104
- refreshError = e?.data?.error_description ?? e?.data?.error ?? e?.message ?? String(e);
105
- return null;
106
- });
107
- else if (tokens?.access_token) liveToken = tokens.access_token;
108
- if (!liveToken) {
109
- const source = describeAuthSource(envKeys, byok);
110
- const detail = refreshError ? `${source} — refresh failed: ${refreshError} (token revoked / invalid — re-run \`gscdump auth login --force\` and update the source above)` : `${source} — no usable access token`;
102
+ const source = byok ?? await getAuth({ interactive: false }).catch((error) => error instanceof Error ? error : new Error(String(error)));
103
+ const current = source instanceof Error ? {
104
+ kind: "failed",
105
+ detail: redactTokens(source.message)
106
+ } : await currentAccessToken(source);
107
+ if (current.kind === "failed") {
111
108
  checks.push({
112
109
  name: "auth",
113
110
  status: "fail",
114
- detail
111
+ detail: `${describeAuthSource(envKeys, byok)}: refresh failed: ${current.detail}. Run \`gscdump auth login --force\` and update the source above.`
115
112
  });
116
113
  return {
117
114
  checks,
118
115
  liveToken: null
119
116
  };
120
117
  }
121
- const info = await ofetch("https://oauth2.googleapis.com/tokeninfo", { query: { access_token: liveToken } }).catch((e) => ({ error: e.message }));
122
- if ("error" in info) {
118
+ const liveToken = current.token;
119
+ const tokenInfo = await fetchTokenInfo(liveToken);
120
+ if (tokenInfo.kind === "invalid") {
123
121
  checks.push({
124
122
  name: "auth",
125
123
  status: "fail",
126
- detail: `tokeninfo failed: ${info.error}`
124
+ detail: `tokeninfo failed: ${tokenInfo.detail}`
127
125
  });
128
126
  return {
129
127
  checks,
130
128
  liveToken: null
131
129
  };
132
130
  }
131
+ const info = tokenInfo.info;
133
132
  checks.push({
134
133
  name: "auth",
135
134
  status: "pass",
@@ -152,12 +151,13 @@ async function checkAuth(envKeys) {
152
151
  status: "pass",
153
152
  detail: `${scopes.length} granted`
154
153
  });
155
- if (tokens?.expiry_date) {
156
- const expiresInMs = tokens.expiry_date - Date.now();
154
+ const savedAfterRefresh = byok ? null : await loadTokens();
155
+ if (savedAfterRefresh?.expiry_date) {
156
+ const expiresInMs = savedAfterRefresh.expiry_date - Date.now();
157
157
  if (expiresInMs < 0) checks.push({
158
158
  name: "auth.expiry",
159
159
  status: "warn",
160
- detail: `expired ${new Date(tokens.expiry_date).toISOString()} — will refresh on next call`
160
+ detail: `expired ${new Date(savedAfterRefresh.expiry_date).toISOString()} — will refresh on next call`
161
161
  });
162
162
  else checks.push({
163
163
  name: "auth.expiry",
@@ -298,7 +298,7 @@ async function checkGscSites() {
298
298
  if (sites instanceof Error) return [{
299
299
  name: "gsc.sites",
300
300
  status: "fail",
301
- detail: `sites() failed: ${sites.message}`
301
+ detail: `sites() failed: ${redactTokens(sites.message)}`
302
302
  }];
303
303
  return describeGscSites(sites, config.defaultSite);
304
304
  }
@@ -311,15 +311,15 @@ function describeGscSites(sites, defaultSite) {
311
311
  detail: `${sites.length} site(s) accessible (${verified} verified)`
312
312
  });
313
313
  if (defaultSite) {
314
- const match = sites.find((s) => s.siteUrl === defaultSite || (s.siteUrl ?? "").includes(defaultSite));
315
- checks.push(match ? {
314
+ const resolution = resolveSiteInput(defaultSite, sites.flatMap((s) => s.siteUrl ? [{ siteUrl: s.siteUrl }] : []));
315
+ checks.push(resolution.kind === "resolved" ? {
316
316
  name: "config.defaultSite",
317
317
  status: "pass",
318
- detail: `${defaultSite} ✓`
318
+ detail: `${resolution.siteUrl} ✓`
319
319
  } : {
320
320
  name: "config.defaultSite",
321
321
  status: "fail",
322
- detail: `${defaultSite} not in verified site list`
322
+ detail: formatSiteResolution(resolution, "account")
323
323
  });
324
324
  }
325
325
  return checks;