@gscdump/cli 4.7.0 → 5.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/dist/auth.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  import { resolveAuthentication } from "./auth-state.mjs";
2
2
  import { getConfigDir, loadConfig, saveConfig } from "./config.mjs";
3
3
  import { pickCliEnvironmentValue, resolveCliEnvironment } from "./environment.mjs";
4
- import { getAppliedEnvKeys, getLoadedEnvPath } from "./env-file.mjs";
5
4
  import { displayPath, logger } from "./utils.mjs";
6
5
  import process from "node:process";
7
6
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
@@ -361,7 +360,7 @@ async function resolveAuth(opts = {}) {
361
360
  return getAuth(opts);
362
361
  }
363
362
  function envSourceLabel(envVar) {
364
- return getAppliedEnvKeys().has(envVar) ? `.env (${envVar})` : `shell env (${envVar})`;
363
+ return `environment (${envVar})`;
365
364
  }
366
365
  function pickEnvSource(...envVars) {
367
366
  return pickCliEnvironmentValue(envVars);
@@ -451,11 +450,6 @@ async function describeAuthProvenance() {
451
450
  else if (tokens) effective = "saved-tokens";
452
451
  else effective = "none";
453
452
  if ((effective === "byok-refresh-token" || effective === "byok-access-token") && tokens) warnings.push("Environment credentials take priority over saved tokens. If you just ran `auth login --force`, unset or update the environment tokens.");
454
- if (effective === "byok-refresh-token" && clientId && refreshTok) {
455
- if (getAppliedEnvKeys().has(clientId.envVar) !== getAppliedEnvKeys().has(refreshTok.envVar)) warnings.push(`client_id and refresh_token come from different sources (${envSourceLabel(clientId.envVar)} vs ${envSourceLabel(refreshTok.envVar)}); they may not match.`);
456
- }
457
- const envFile = getLoadedEnvPath();
458
- if (envFile && getAppliedEnvKeys().size > 0) warnings.push(`Loaded ${getAppliedEnvKeys().size} var(s) from ${displayPath(envFile)}.`);
459
453
  return {
460
454
  rows,
461
455
  effective,
package/dist/cli.d.mts CHANGED
@@ -2,7 +2,6 @@ import { CliRuntime, CreateCliRuntimeOptions, createCliRuntime } from "./runtime
2
2
  export declare const main: import("citty").CommandDef<import("citty").ArgsDef>;
3
3
  export interface RunCliOptions {
4
4
  rawArgs?: string[];
5
- loadEnv?: boolean;
6
5
  environment?: Record<string, string | undefined>;
7
6
  runtime?: CliRuntime;
8
7
  }
package/dist/cli.mjs CHANGED
@@ -5,7 +5,6 @@ import { parseAuthMode } from "./auth-state.mjs";
5
5
  import { checkCliArgs } from "./cli-args.mjs";
6
6
  import { resolveCliEnvironment } from "./environment.mjs";
7
7
  import { applyProfileFromCli } from "./commands/profile-selection.mjs";
8
- import { loadEnvFromCwd } from "./env-file.mjs";
9
8
  import { resolveOutputOptions, terminalOutputOptions } from "./render/terminal.mjs";
10
9
  import { VERSION, setNoColor, showSplash, withConfiguredOutput } from "./utils.mjs";
11
10
  import process from "node:process";
@@ -98,7 +97,6 @@ async function runCli(opts = {}) {
98
97
  return runWithCliRuntime(runtime, async () => {
99
98
  let rawArgs = [...input];
100
99
  try {
101
- if (opts.loadEnv !== false) loadEnvFromCwd();
102
100
  rawArgs = prepareCliArgs(input);
103
101
  runtime.rawArgs = [...rawArgs];
104
102
  const argumentError = await checkCliArgs(main, rawArgs);
@@ -1,7 +1,7 @@
1
1
  import { bingCommandMeta } from "../command-meta.mjs";
2
2
  import { useCliRuntime } from "../runtime.mjs";
3
3
  import { resolveAuthentication, saveAuthentication } from "../auth-state.mjs";
4
- import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
4
+ import { OUTPUT_ARGS, applyOutputMode, logger, plural } 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";
7
7
  import { dumpHostedBingSite, hostedBingClient, inspectHostedBingUrl, listHostedBingSites, resolveHostedBingSites } from "../bing-hosted.mjs";
@@ -171,7 +171,7 @@ const bingCommand = defineCommand({
171
171
  authenticated: true,
172
172
  verifiedSites: sites.filter((site) => site.isVerified).length
173
173
  }));
174
- else logger.success(`Bing authenticated. ${sites.filter((site) => site.isVerified).length} verified sites.`);
174
+ else logger.success(`Bing authenticated. ${plural(sites.filter((site) => site.isVerified).length, "verified site", "verified sites")}.`);
175
175
  }
176
176
  }),
177
177
  sites: defineCommand({
@@ -250,7 +250,7 @@ const bingCommand = defineCommand({
250
250
  for (const site of sites) {
251
251
  const summary = await dumpHostedBingSite(authentication, site, args.out, options);
252
252
  summaries.push(summary);
253
- if (!json) logger.success(`Bing ${site.siteUrl}: ${summary.files.length} files exported to ${args.out}.`);
253
+ if (!json) logger.success(`Bing ${site.siteUrl}: ${plural(summary.files.length, "file", "files")} exported to ${args.out}.`);
254
254
  }
255
255
  if (json) console.log(JSON.stringify(args["all-sites"] ? {
256
256
  searchEngine: "bing",
@@ -267,7 +267,7 @@ const bingCommand = defineCommand({
267
267
  for (const site of sites) {
268
268
  const summary = await dumpBingSite(client, site, args.out, options);
269
269
  summaries.push(summary);
270
- if (!json) logger.success(`Bing ${site}: ${summary.files.length} files exported to ${args.out}.`);
270
+ if (!json) logger.success(`Bing ${site}: ${plural(summary.files.length, "file", "files")} exported to ${args.out}.`);
271
271
  }
272
272
  if (json) console.log(JSON.stringify(args["all-sites"] ? {
273
273
  searchEngine: "bing",
@@ -1,8 +1,7 @@
1
1
  import { doctorCommandMeta } from "../command-meta.mjs";
2
2
  import { getHostedAccount, resolveAuthentication } from "../auth-state.mjs";
3
3
  import { loadConfig } from "../config.mjs";
4
- import { resolveCliEnvironment } from "../environment.mjs";
5
- import { parseEnvFile } from "../env-file.mjs";
4
+ import { pickCliEnvironmentValue, resolveCliEnvironment } from "../environment.mjs";
6
5
  import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs";
7
6
  import { getAuth, isStaleServiceAccountPointer, loadTokens, resolveAuth, resolveBYOK, resolveServiceAccount } from "../auth.mjs";
8
7
  import { createLocalStore } from "../local-store.mjs";
@@ -12,73 +11,19 @@ import { currentAccessToken, fetchTokenInfo, redactTokens } from "../token-info.
12
11
  import process from "node:process";
13
12
  import { defineCommand } from "citty";
14
13
  import fs from "node:fs/promises";
15
- import path from "node:path";
16
14
  import { googleSearchConsole } from "gscdump/client";
17
15
  import { resolveSiteInput } from "gscdump";
18
16
  import { ofetch } from "ofetch";
19
17
  const FETCH_TIMEOUT_MS = 5e3;
20
18
  const TIME_SKEW_WARN_MS = 3e5;
21
19
  const WATERMARK_STALE_DAYS_WARN = 7;
22
- const RELEVANT_ENV_KEYS = [
23
- "GSC_ACCESS_TOKEN",
24
- "GSC_CLIENT_ID",
25
- "GSC_CLIENT_SECRET",
26
- "GSC_REFRESH_TOKEN",
27
- "GOOGLE_ACCESS_TOKEN",
28
- "GOOGLE_CLIENT_ID",
29
- "GOOGLE_CLIENT_SECRET",
30
- "GOOGLE_REFRESH_TOKEN",
31
- "GOOGLE_APPLICATION_CREDENTIALS",
32
- "GSC_SERVICE_ACCOUNT_JSON"
33
- ];
34
- function redact(v) {
35
- if (!v) return "<missing>";
36
- if (v.length <= 6) return "***";
37
- return `***${v.slice(-6)}`;
38
- }
39
- async function checkEnv() {
40
- const envPath = path.join(process.cwd(), ".env");
41
- const parsed = parseEnvFile(envPath);
42
- if (!parsed) return {
43
- checks: [{
44
- name: "env",
45
- status: "info",
46
- detail: "no .env (using shell env / saved tokens)"
47
- }],
48
- envKeys: /* @__PURE__ */ new Set()
49
- };
50
- const relevant = RELEVANT_ENV_KEYS.filter((k) => parsed[k] !== void 0);
51
- const envKeys = new Set(relevant);
52
- if (relevant.length === 0) return {
53
- checks: [{
54
- name: "env",
55
- status: "info",
56
- detail: `${displayPath(envPath)} found, no auth vars`
57
- }],
58
- envKeys
59
- };
60
- const inventory = relevant.map((k) => {
61
- const v = parsed[k];
62
- if (k.endsWith("CLIENT_ID")) return `${k}=${v}`;
63
- return `${k}=${redact(v)}`;
64
- });
65
- return {
66
- checks: [{
67
- name: "env",
68
- status: "info",
69
- detail: `${displayPath(envPath)} → ${inventory.join(", ")} (not validated — see auth)`
70
- }],
71
- envKeys
72
- };
73
- }
74
- function describeAuthSource(envKeys, byok) {
20
+ function describeAuthSource(byok) {
75
21
  if (!byok) return "saved tokens";
76
22
  const isAccessToken = typeof byok === "string";
77
- const driver = isAccessToken ? "GSC_ACCESS_TOKEN" : "GSC_REFRESH_TOKEN";
78
- const source = (isAccessToken ? ["GSC_ACCESS_TOKEN", "GOOGLE_ACCESS_TOKEN"] : ["GSC_REFRESH_TOKEN", "GOOGLE_REFRESH_TOKEN"]).some((k) => envKeys.has(k)) ? ".env" : "shell env";
79
- return `environment credentials ${isAccessToken ? "(access-token)" : "(refresh-token)"} from ${source} via ${driver}`;
23
+ const driver = pickCliEnvironmentValue(isAccessToken ? ["GSC_ACCESS_TOKEN", "GOOGLE_ACCESS_TOKEN"] : ["GSC_REFRESH_TOKEN", "GOOGLE_REFRESH_TOKEN"])?.envVar ?? (isAccessToken ? "GSC_ACCESS_TOKEN" : "GSC_REFRESH_TOKEN");
24
+ return `environment credentials ${isAccessToken ? "(access-token)" : "(refresh-token)"} via ${driver}`;
80
25
  }
81
- async function checkAuth(envKeys) {
26
+ async function checkAuth() {
82
27
  const checks = [];
83
28
  const serviceAccount = await resolveServiceAccount().catch((error) => {
84
29
  if (isStaleServiceAccountPointer(error)) {
@@ -153,7 +98,7 @@ async function checkAuth(envKeys) {
153
98
  checks.push({
154
99
  name: "auth",
155
100
  status: "fail",
156
- detail: `${describeAuthSource(envKeys, byok)}: refresh failed: ${current.detail}. Run \`gscdump auth login --force\` and update the source above.`
101
+ detail: `${describeAuthSource(byok)}: refresh failed: ${current.detail}. Run \`gscdump auth login --force\` and update the source above.`
157
102
  });
158
103
  return {
159
104
  checks,
@@ -177,7 +122,7 @@ async function checkAuth(envKeys) {
177
122
  checks.push({
178
123
  name: "auth",
179
124
  status: "pass",
180
- detail: describeAuthSource(envKeys, byok)
125
+ detail: describeAuthSource(byok)
181
126
  });
182
127
  if (info.email) checks.push({
183
128
  name: "auth.account",
@@ -410,9 +355,9 @@ async function checkHostedConnection(authentication) {
410
355
  ...sitesChecks
411
356
  ];
412
357
  }
413
- async function checkLocalConnection(envKeys) {
358
+ async function checkLocalConnection() {
414
359
  const [authResult, timeChecks, gscApi, indexingApi, siteVerificationApi] = await Promise.all([
415
- checkAuth(envKeys),
360
+ checkAuth(),
416
361
  checkTimeSkew(),
417
362
  checkApiReachable("gsc.api", "https://searchconsole.googleapis.com/$discovery/rest?version=v1"),
418
363
  checkApiReachable("indexing.api", "https://indexing.googleapis.com/$discovery/rest?version=v3"),
@@ -439,14 +384,12 @@ const doctorCommand = defineCommand({
439
384
  const { json } = applyOutputMode(args);
440
385
  const { dataDir } = await createCommandContext();
441
386
  const authentication = await resolveAuthentication();
442
- const envResult = await checkEnv();
443
387
  const [connectionChecks, dataDirChecks, watermarkChecks] = await Promise.all([
444
- authentication._tag === "Hosted" ? checkHostedConnection(authentication) : checkLocalConnection(envResult.envKeys),
388
+ authentication._tag === "Hosted" ? checkHostedConnection(authentication) : checkLocalConnection(),
445
389
  checkDataDir(dataDir),
446
390
  checkStoreWatermarks(dataDir)
447
391
  ]);
448
392
  const all = [
449
- ...envResult.checks,
450
393
  ...connectionChecks,
451
394
  ...dataDirChecks,
452
395
  ...watermarkChecks
@@ -2,17 +2,13 @@ import { initCommandMeta } from "../command-meta.mjs";
2
2
  import { useCliRuntime } from "../runtime.mjs";
3
3
  import { saveAuthentication } from "../auth-state.mjs";
4
4
  import { defaultDataDir, loadConfig, saveConfig } from "../config.mjs";
5
- import { applyCliEnvironment } from "../environment.mjs";
6
- import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs";
7
- import { ACCESS_NOT_SET_UP, authenticate, getAuth, loadTokens, resolveBYOK, resolveServiceAccount, saveTokens } from "../auth.mjs";
5
+ import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
6
+ import { ACCESS_NOT_SET_UP, getAuth, loadTokens, resolveBYOK, resolveServiceAccount } from "../auth.mjs";
8
7
  import { loginHosted } from "./auth.mjs";
9
8
  import process from "node:process";
10
9
  import { defineCommand } from "citty";
11
- import fs from "node:fs/promises";
12
- import path from "node:path";
13
- import { confirm, isCancel, text } from "@clack/prompts";
10
+ import { isCancel, text } from "@clack/prompts";
14
11
  import { googleSearchConsole } from "gscdump/client";
15
- const ENV_LINE_RE = /^([^=]+)=(.*)$/;
16
12
  function canPrompt() {
17
13
  return process.stdin.isTTY === true;
18
14
  }
@@ -27,24 +23,6 @@ async function promptDataDir(existing) {
27
23
  if (isCancel(answer)) process.exit(1);
28
24
  return String(answer) || fallback;
29
25
  }
30
- async function loadEnvFile() {
31
- const envPath = path.join(process.cwd(), ".env");
32
- const content = await fs.readFile(envPath, "utf-8").catch(() => null);
33
- if (!content) return null;
34
- const env = {};
35
- for (const line of content.split("\n")) {
36
- const trimmed = line.trim();
37
- if (!trimmed || trimmed.startsWith("#")) continue;
38
- const match = trimmed.match(ENV_LINE_RE);
39
- if (match) {
40
- const key = match[1].trim();
41
- let value = match[2].trim();
42
- if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
43
- env[key] = value;
44
- }
45
- }
46
- return env;
47
- }
48
26
  const initCommand = defineCommand({
49
27
  meta: initCommandMeta,
50
28
  args: {
@@ -121,40 +99,6 @@ const initCommand = defineCommand({
121
99
  printNextSteps();
122
100
  return;
123
101
  }
124
- const envFile = await loadEnvFile();
125
- const envCid = envFile?.GSC_CLIENT_ID ?? envFile?.GOOGLE_CLIENT_ID;
126
- const envSec = envFile?.GSC_CLIENT_SECRET ?? envFile?.GOOGLE_CLIENT_SECRET;
127
- const envRef = envFile?.GSC_REFRESH_TOKEN ?? envFile?.GOOGLE_REFRESH_TOKEN;
128
- const envAcc = envFile?.GSC_ACCESS_TOKEN ?? envFile?.GOOGLE_ACCESS_TOKEN;
129
- if (envCid && envSec && envRef) {
130
- logger.info("Found .env file with credentials");
131
- applyCliEnvironment({
132
- GOOGLE_ACCESS_TOKEN: envAcc,
133
- GOOGLE_CLIENT_ID: envCid,
134
- GOOGLE_CLIENT_SECRET: envSec,
135
- GOOGLE_REFRESH_TOKEN: envRef
136
- });
137
- await saveConfig({
138
- ...config,
139
- clientId: envCid,
140
- clientSecret: envSec,
141
- dataDir: config.dataDir ?? defaultDataDir()
142
- });
143
- const creds = (await authenticate({
144
- clientId: envCid,
145
- clientSecret: envSec
146
- }, false)).credentials;
147
- if (creds.access_token) await saveTokens({
148
- access_token: creds.access_token,
149
- refresh_token: creds.refresh_token || envRef,
150
- expiry_date: creds.expiry_date
151
- });
152
- await saveAuthentication({ _tag: "Local" });
153
- console.log();
154
- logger.success("Setup complete using .env credentials.");
155
- printNextSteps();
156
- return;
157
- }
158
102
  if (!canPrompt()) {
159
103
  if (!await loadTokens()) {
160
104
  logger.error(`Init cannot prompt without a terminal.\n${ACCESS_NOT_SET_UP}`);
@@ -180,7 +124,6 @@ const initCommand = defineCommand({
180
124
  });
181
125
  const sites = await runSmokeTest(await getAuth({ interactive: true }));
182
126
  await saveAuthentication({ _tag: "Local" });
183
- if (config.clientId && config.clientSecret) await maybeWriteEnvFile(config.clientId, config.clientSecret);
184
127
  console.log();
185
128
  logger.success("Setup complete.");
186
129
  printNextSteps(sites);
@@ -229,32 +172,4 @@ async function runSmokeTest(oauth) {
229
172
  logger.success(`Verified: ${sites.length} GSC site(s) accessible`);
230
173
  return sites.flatMap((site) => site.siteUrl ? [site.siteUrl] : []);
231
174
  }
232
- async function maybeWriteEnvFile(clientId, clientSecret) {
233
- const tokens = await loadTokens();
234
- if (!tokens?.refresh_token) return;
235
- const wants = await confirm({
236
- message: "Write a `.env` file with these credentials? (handy for CI / other machines)",
237
- initialValue: false
238
- });
239
- if (isCancel(wants) || !wants) return;
240
- const envPath = path.join(process.cwd(), ".env");
241
- if (await fs.stat(envPath).then(() => true).catch(() => false)) {
242
- const overwrite = await confirm({
243
- message: `${displayPath(envPath)} exists. Overwrite?`,
244
- initialValue: false
245
- });
246
- if (isCancel(overwrite) || !overwrite) {
247
- logger.info(`Skipped — keep credentials at ${displayPath(envPath)} manually if needed`);
248
- return;
249
- }
250
- }
251
- const content = [
252
- `GSC_CLIENT_ID=${clientId}`,
253
- `GSC_CLIENT_SECRET=${clientSecret}`,
254
- `GSC_REFRESH_TOKEN=${tokens.refresh_token}`,
255
- ""
256
- ].join("\n");
257
- await fs.writeFile(envPath, content, { mode: 384 });
258
- logger.success(`Wrote ${displayPath(envPath)}`);
259
- }
260
175
  export { initCommand, printHostedNextSteps, printNextSteps, runSmokeTest };
@@ -1,4 +1,5 @@
1
1
  import { queryCommandMeta } from "../command-meta.mjs";
2
+ import { queryHostedRows } from "../hosted-query.mjs";
2
3
  import { useCliRuntime } from "../runtime.mjs";
3
4
  import { resolveAuthentication } from "../auth-state.mjs";
4
5
  import { loadConfig } from "../config.mjs";
@@ -12,14 +13,13 @@ import { renderTable } from "../render/layout.mjs";
12
13
  import { asRecord, columnsFor } from "../render/analysis.mjs";
13
14
  import { resolveHostedSite } from "../hosted-site.mjs";
14
15
  import { FILTER_DIMS, filterDimensions, parseFilterArgs, toLiveFilter, toLocalFilter } from "../filters.mjs";
15
- import { queryHostedRows } from "../hosted-query.mjs";
16
16
  import { renderQuery } from "../render/query.mjs";
17
17
  import { openSqlViews, referencedTables } from "../sql-views.mjs";
18
18
  import process from "node:process";
19
19
  import { defineCommand } from "citty";
20
20
  import fs from "node:fs/promises";
21
21
  import { and, between, country, date, device, gsc, hour, page, query, searchAppearance } from "gscdump/query";
22
- import { getLatestGscDate } from "gscdump/dates";
22
+ import { getLatestGscDate, toIsoDate } from "gscdump/dates";
23
23
  import { cancel, isCancel, multiselect, text } from "@clack/prompts";
24
24
  import { collectSpans } from "@gscdump/engine/profile";
25
25
  import { inferDataset, isDatasetResolvable } from "gscdump/query/plan";
@@ -431,7 +431,7 @@ const queryCommand = defineCommand({
431
431
  end: endDate
432
432
  },
433
433
  total: result.rows.length,
434
- data: result.rows,
434
+ data: result.rows.map(withIsoDate),
435
435
  meta: { source: "local" }
436
436
  },
437
437
  format,
@@ -547,7 +547,14 @@ function buildLocalState(dimNames, startDate, endDate, rowLimit, dimensionFilter
547
547
  const dims = dimNames.map((d) => DIM_COLUMNS[d]).filter((c) => Boolean(c));
548
548
  const dateFilter = between(date, startDate, endDate);
549
549
  const filter = dimensionFilter ? and(dateFilter, dimensionFilter) : dateFilter;
550
- return gsc.select(...dims).where(filter).limit(rowLimit).getState();
550
+ const builder = gsc.select(...dims).where(filter);
551
+ return (dimNames.includes("date") ? builder.orderBy(date, "asc") : builder).limit(rowLimit).getState();
552
+ }
553
+ function withIsoDate(row) {
554
+ return typeof row.date === "number" ? {
555
+ ...row,
556
+ date: toIsoDate(new Date(row.date))
557
+ } : row;
551
558
  }
552
559
  async function runSqlMode(opts) {
553
560
  const ctx = await createCommandContext({ needsStore: true });
@@ -1,5 +1,5 @@
1
1
  import { sitemapsCommandMeta } from "../command-meta.mjs";
2
- import { OUTPUT_ARGS, applyOutputMode, logger, parseIntegerOption } from "../utils.mjs";
2
+ import { OUTPUT_ARGS, applyOutputMode, logger, parseIntegerOption, plural } from "../utils.mjs";
3
3
  import { createCommandContext } from "../context.mjs";
4
4
  import { discoverLiveSitemap, loadSitemapUrls } from "../sitemap.mjs";
5
5
  import { HOSTED_ARGS, resolveHostedSite } from "../hosted-site.mjs";
@@ -59,12 +59,12 @@ const listCommand = defineCommand({
59
59
  logger.warn("No sitemaps found");
60
60
  return;
61
61
  }
62
- logger.success(`Found ${sitemaps.length} sitemaps:`);
62
+ logger.success(`Found ${plural(sitemaps.length, "sitemap", "sitemaps")}:`);
63
63
  console.log();
64
64
  for (const sm of sitemaps) {
65
65
  const pending = sm.isPending ? " \x1B[33m(pending)\x1B[0m" : "";
66
- const errors = sm.errors ? ` \x1B[31m${sm.errors} errors\x1B[0m` : "";
67
- const warnings = sm.warnings ? ` \x1B[33m${sm.warnings} warnings\x1B[0m` : "";
66
+ const errors = sm.errors ? ` \x1B[31m${plural(sm.errors, "error", "errors")}\x1B[0m` : "";
67
+ const warnings = sm.warnings ? ` \x1B[33m${plural(sm.warnings, "warning", "warnings")}\x1B[0m` : "";
68
68
  const submitted = sm.lastSubmitted ? ` \x1B[90msubmitted ${sm.lastSubmitted}\x1B[0m` : "";
69
69
  console.log(` ${sm.path}${pending}${errors}${warnings}${submitted}`);
70
70
  }
@@ -1,6 +1,6 @@
1
1
  import { sitesCommandMeta } from "../command-meta.mjs";
2
2
  import { LOCAL_MODE_REQUIRED, formatHostedSync, getHostedAccount, resolveAuthentication } from "../auth-state.mjs";
3
- import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
3
+ import { OUTPUT_ARGS, applyOutputMode, logger, plural } from "../utils.mjs";
4
4
  import { createCommandContext, formatSiteResolution } from "../context.mjs";
5
5
  import process from "node:process";
6
6
  import { defineCommand } from "citty";
@@ -362,7 +362,7 @@ const verifyListCommand = defineCommand({
362
362
  logger.warn("No verified WebResources found");
363
363
  return;
364
364
  }
365
- logger.success(`${resources.length} verified WebResources:`);
365
+ logger.success(`${plural(resources.length, "verified WebResource", "verified WebResources")}:`);
366
366
  console.log();
367
367
  for (const r of resources) {
368
368
  const id = r.id ?? "?";
@@ -441,7 +441,7 @@ async function runListHostedSites(args, authentication) {
441
441
  logger.warn("Your hosted record has no Sites. Connect a Site at https://gscdump.com/app/onboarding?step=connect-sites.");
442
442
  return;
443
443
  }
444
- logger.success(`Found ${sites.length} hosted sites:`);
444
+ logger.success(`Found ${plural(sites.length, "hosted Site", "hosted Sites")}:`);
445
445
  console.log();
446
446
  for (const site of sites) console.log(` ${site.siteUrl} \x1B[90m(${site.siteId})\x1B[0m hosted sync ${formatHostedSync(site)}`);
447
447
  }
@@ -470,10 +470,10 @@ async function runListSites(args) {
470
470
  return;
471
471
  }
472
472
  if (sites.length === 0) {
473
- logger.warn(ownerOnly ? "No owned sites found" : "No verified sites found");
473
+ logger.warn(ownerOnly ? "No owned Sites found" : "No verified Sites found");
474
474
  return;
475
475
  }
476
- logger.success(`Found ${sites.length} ${ownerOnly ? "owned" : "verified"} sites:`);
476
+ logger.success(`Found ${ownerOnly ? plural(sites.length, "owned Site", "owned Sites") : plural(sites.length, "verified Site", "verified Sites")}:`);
477
477
  console.log();
478
478
  for (const site of sites) {
479
479
  const perm = site.permissionLevel === "siteOwner" ? "\x1B[32m" : "\x1B[90m";
@@ -492,10 +492,10 @@ async function runListSites(args) {
492
492
  return;
493
493
  }
494
494
  if (sites.length === 0) {
495
- logger.warn(ownerOnly ? "No owned sites found" : "No verified sites found");
495
+ logger.warn(ownerOnly ? "No owned Sites found" : "No verified Sites found");
496
496
  return;
497
497
  }
498
- logger.success(`Found ${sites.length} ${ownerOnly ? "owned " : ""}sites:`);
498
+ logger.success(`Found ${ownerOnly ? plural(sites.length, "owned Site", "owned Sites") : plural(sites.length, "Site", "Sites")}:`);
499
499
  console.log();
500
500
  for (const site of sites) {
501
501
  const perm = site.permissionLevel === "siteOwner" ? "\x1B[32m" : "\x1B[90m";
package/dist/coverage.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { quotaLedgerPath, quotaStatus, readLedgerState } from "./quota-ledger.mjs";
2
+ import { plural } from "./utils.mjs";
2
3
  import { INSPECTION_QPD_PER_PROPERTY } from "./inspection-record.mjs";
3
4
  import { inspectionCandidates, loadInspectionState } from "./local-entities.mjs";
4
5
  import { datesForJob, jobWindowStart } from "./sync-plan.mjs";
@@ -117,9 +118,6 @@ function hasSyncedDays(states, table, searchType) {
117
118
  function n(value) {
118
119
  return value.toLocaleString("en-US");
119
120
  }
120
- function plural(count, one, many) {
121
- return `${n(count)} ${count === 1 ? one : many}`;
122
- }
123
121
  function clock(at) {
124
122
  return new Date(at).toLocaleString("en-US", {
125
123
  hour: "numeric",
@@ -23,8 +23,4 @@ function pickCliEnvironmentValue(names, values = useCliRuntime().environment) {
23
23
  }
24
24
  return null;
25
25
  }
26
- function applyCliEnvironment(updates, values = useCliRuntime().environment) {
27
- for (const [key, value] of Object.entries(updates)) if (value === void 0) delete values[key];
28
- else values[key] = value;
29
- }
30
- export { applyCliEnvironment, pickCliEnvironmentValue, resolveCliEnvironment };
26
+ export { pickCliEnvironmentValue, resolveCliEnvironment };
@@ -1,6 +1,8 @@
1
1
  import { isUsageError } from "./command-registry.mjs";
2
+ import { describeRecordReadRefusal } from "./hosted-query.mjs";
2
3
  import { quotaStopOf } from "./quota-ledger.mjs";
3
4
  import { isAnalysisError } from "@gscdump/analysis/errors";
5
+ import { parseRecordReadRefusal } from "@gscdump/contracts";
4
6
  import { isEngineError } from "@gscdump/engine/errors";
5
7
  import { classifyError } from "gscdump/errors";
6
8
  import { isQueryError } from "gscdump/query";
@@ -103,6 +105,10 @@ const DEFECT_ERRORS = /* @__PURE__ */ new Set([
103
105
  "EvalError",
104
106
  "URIError"
105
107
  ]);
108
+ function recordReadRefusalOf(error) {
109
+ if (!(error instanceof Error) || error.name !== "GscdumpV1Error") return null;
110
+ return parseRecordReadRefusal(error.details);
111
+ }
106
112
  function describeCliError(error) {
107
113
  if (isUsageError(error)) return {
108
114
  kind: "usage",
@@ -136,6 +142,12 @@ function describeCliError(error) {
136
142
  hint: error.mode === "local" ? "Pass --live to run against the GSC API." : "",
137
143
  showAuthSources: false
138
144
  };
145
+ const refusal = recordReadRefusalOf(error);
146
+ if (refusal) return {
147
+ kind: "expected",
148
+ ...describeRecordReadRefusal(refusal),
149
+ showAuthSources: false
150
+ };
139
151
  const classified = classifyError(error);
140
152
  if (classified.message === HOSTED_KEY_REJECTED) return {
141
153
  kind: "expected",
@@ -28,4 +28,26 @@ async function queryHostedRows(client, siteId, state) {
28
28
  }
29
29
  return rows;
30
30
  }
31
- export { queryHostedRows };
31
+ function describeRecordReadRefusal(refusal) {
32
+ const running = refusal.syncStatus === "syncing" || refusal.syncStatus === "pending";
33
+ if (refusal.reason === "record_not_ready") return {
34
+ message: "The Site's record is not readable yet.",
35
+ hint: refusal.syncStatus === "synced" && refusal.lastSyncAt !== void 0 ? `Sync finished at ${utcMinute(refusal.lastSyncAt)}. gscdump prepares the record for reads after Sync. Try again in a few minutes.` : running ? "Sync is still running. Run `gscdump sites` to see its progress, then try again when it finishes." : "Try again in a few minutes. Run `gscdump sites` to see the Sync state."
36
+ };
37
+ return {
38
+ message: `The Site's record does not hold ${refusal.missingStart === refusal.missingEnd ? refusal.missingStart : `${refusal.missingStart} to ${refusal.missingEnd}`}.`,
39
+ hint: rangeHint(refusal, running)
40
+ };
41
+ }
42
+ function rangeHint(refusal, running) {
43
+ const { oldestDateSynced: oldest, newestDateSynced: newest } = refusal;
44
+ const synced = oldest !== void 0 && newest !== void 0;
45
+ if (synced && refusal.missingStart >= oldest && refusal.missingEnd <= newest) return "Sync covers these days, but the record does not hold them yet. Pick an earlier end date with --end, or try again later.";
46
+ const covers = synced ? `Sync covers ${oldest} to ${newest}. ` : "";
47
+ if (running) return `${covers}Sync is still running. Try again when it finishes.`;
48
+ return synced ? `${covers}Pick dates in that range with --start and --end.` : "Run `gscdump sites` to see the Sync state.";
49
+ }
50
+ function utcMinute(unixSeconds) {
51
+ return `${(/* @__PURE__ */ new Date(unixSeconds * 1e3)).toISOString().slice(0, 16).replace("T", " ")} UTC`;
52
+ }
53
+ export { describeRecordReadRefusal, queryHostedRows };
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { SearchTypes } from "gscdump/query";
3
+ import "gscdump/dates";
3
4
  const periodSchema = z.object({
4
5
  start: z.string().describe("Start date (YYYY-MM-DD)"),
5
6
  end: z.string().describe("End date (YYYY-MM-DD)")
package/dist/package.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var version = "4.7.0";
1
+ var version = "5.0.0";
2
2
  export { version };
@@ -21,7 +21,7 @@ function renderQuery(input, options) {
21
21
  if (clicks !== -1) columns[clicks] = barColumn(input.rows, "clicks", options);
22
22
  }
23
23
  return [
24
- ...textLines(`${parseGscSiteUrl(input.site).hostname} / query`, options, "accent"),
24
+ ...textLines(`${parseGscSiteUrl(input.site).hostname} / ${input.dimensions.join(", ")}`, options, "accent"),
25
25
  ...textLines(`${input.start} to ${input.end}`, options, "muted"),
26
26
  "",
27
27
  ...renderTable(input.rows, columns, options)
@@ -1,5 +1,5 @@
1
- import { addDays, getDateRange } from "gscdump/dates";
2
1
  import { searchTypeSupportsDimensions, searchTypeSupportsQueries } from "@gscdump/contracts";
2
+ import { addDays, getDateRange } from "gscdump/dates";
3
3
  const TABLE_NEEDS = {
4
4
  pages: {
5
5
  queries: false,
package/dist/utils.mjs CHANGED
@@ -156,6 +156,9 @@ function displayPath(absPath) {
156
156
  if (absPath.startsWith(`${home}/`)) return `~/${absPath.slice(home.length + 1)}`;
157
157
  return absPath;
158
158
  }
159
+ function plural(count, one, many) {
160
+ return `${count.toLocaleString("en-US")} ${count === 1 ? one : many}`;
161
+ }
159
162
  function formatAge(ms) {
160
163
  const delta = Date.now() - ms;
161
164
  if (delta < 6e4) return "just now";
@@ -193,4 +196,4 @@ async function readUrlList(input) {
193
196
  for await (const chunk of process.stdin) chunks.push(chunk);
194
197
  return parseUrlLines(Buffer.concat(chunks).toString("utf-8"));
195
198
  }
196
- export { ALL_SEARCH_TYPES, OUTPUT_ARGS, VERSION, applyOutputMode, clearLine, color, cyan, dim, displayPath, formatAge, isColorEnabled, logger, parseFetchBudget, parseIntegerOption, parseNameList, parseSearchType, progressBar, readUrlList, red, runWithConcurrency, setNoColor, setQuiet, showSplash, toCSV, withConfiguredOutput };
199
+ export { ALL_SEARCH_TYPES, OUTPUT_ARGS, VERSION, applyOutputMode, clearLine, color, cyan, dim, displayPath, formatAge, isColorEnabled, logger, parseFetchBudget, parseIntegerOption, parseNameList, parseSearchType, plural, progressBar, readUrlList, red, runWithConcurrency, setNoColor, setQuiet, showSplash, toCSV, withConfiguredOutput };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/cli",
3
3
  "type": "module",
4
- "version": "4.7.0",
4
+ "version": "5.0.0",
5
5
  "description": "CLI for Google Search Console and Bing with hosted or local authentication, data exports, and an MCP server",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -44,16 +44,16 @@
44
44
  "dependencies": {
45
45
  "@clack/prompts": "^1.8.1",
46
46
  "@duckdb/node-api": "1.5.5-r.5",
47
- "@gscdump/analysis": "^4.7.0",
48
- "@gscdump/contracts": "^4.7.0",
49
- "@gscdump/engine": "^4.7.0",
50
- "@gscdump/engine-gsc-api": "^4.7.0",
51
- "@gscdump/sdk": "^4.7.0",
47
+ "@gscdump/analysis": "^5.0.0",
48
+ "@gscdump/contracts": "^5.0.0",
49
+ "@gscdump/engine": "^5.0.0",
50
+ "@gscdump/engine-gsc-api": "^5.0.0",
51
+ "@gscdump/sdk": "^5.0.0",
52
52
  "@modelcontextprotocol/sdk": "^1.30.1",
53
53
  "citty": "^0.2.2",
54
54
  "consola": "^3.4.2",
55
55
  "google-auth-library": "^11.1.0",
56
- "gscdump": "^4.7.0",
56
+ "gscdump": "^5.0.0",
57
57
  "ofetch": "^1.5.1",
58
58
  "open": "^11.0.4",
59
59
  "proper-lockfile": "^4.1.2",
package/dist/env-file.mjs DELETED
@@ -1,53 +0,0 @@
1
- import { useCliRuntime } from "./runtime.mjs";
2
- import process from "node:process";
3
- import path from "node:path";
4
- import fs from "node:fs";
5
- const ENV_LINE_RE = /^([^=]+)=(.*)$/;
6
- function parseEnvFile(envPath) {
7
- let content;
8
- try {
9
- content = fs.readFileSync(envPath, "utf-8");
10
- } catch {
11
- return null;
12
- }
13
- const env = {};
14
- for (const line of content.split("\n")) {
15
- const trimmed = line.trim();
16
- if (!trimmed || trimmed.startsWith("#")) continue;
17
- const match = trimmed.match(ENV_LINE_RE);
18
- if (!match) continue;
19
- const key = match[1].trim();
20
- let value = match[2].trim();
21
- if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
22
- env[key] = value;
23
- }
24
- return env;
25
- }
26
- const appliedEnvKeys = /* @__PURE__ */ new Set();
27
- let loadedEnvPath = null;
28
- function getAppliedEnvKeys() {
29
- return appliedEnvKeys;
30
- }
31
- function getLoadedEnvPath() {
32
- return loadedEnvPath;
33
- }
34
- function loadEnvFromCwd() {
35
- return loadEnvFile({
36
- cwd: process.cwd(),
37
- env: useCliRuntime().environment
38
- });
39
- }
40
- function loadEnvFile(opts) {
41
- const envPath = path.join(opts.cwd, ".env");
42
- const parsed = parseEnvFile(envPath);
43
- if (!parsed) return [];
44
- loadedEnvPath = envPath;
45
- const applied = [];
46
- for (const [key, value] of Object.entries(parsed)) if (opts.env[key] === void 0) {
47
- opts.env[key] = value;
48
- applied.push(key);
49
- appliedEnvKeys.add(key);
50
- }
51
- return applied;
52
- }
53
- export { getAppliedEnvKeys, getLoadedEnvPath, loadEnvFile, loadEnvFromCwd, parseEnvFile };