@gscdump/cli 4.8.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import { papercutCommandMeta } from "../command-meta.mjs";
2
1
  import { useCliRuntime } from "../runtime.mjs";
2
+ import { papercutCommandMeta } from "../command-meta.mjs";
3
3
  import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
4
4
  import { buildPapercutBody, resolvePapercutUrl, submitPapercut } from "../papercut.mjs";
5
5
  import process from "node:process";
@@ -1,5 +1,5 @@
1
- import { profileCommandMeta } from "../command-meta.mjs";
2
1
  import { useCliRuntime } from "../runtime.mjs";
2
+ import { profileCommandMeta } from "../command-meta.mjs";
3
3
  import { getConfigDir, setConfigDir } from "../config.mjs";
4
4
  import { resolveCliEnvironment } from "../environment.mjs";
5
5
  import { ACTIVE_MARKER, PROFILES_DIR, ROOT_DIR, getProfileDir, readActiveMarkerSync } from "./profile-selection.mjs";
@@ -1,9 +1,10 @@
1
- import { queryCommandMeta } from "../command-meta.mjs";
2
1
  import { useCliRuntime } from "../runtime.mjs";
3
2
  import { resolveAuthentication } from "../auth-state.mjs";
3
+ import { queryCommandMeta } from "../command-meta.mjs";
4
4
  import { loadConfig } from "../config.mjs";
5
+ import { queryHostedRows } from "../hosted-query.mjs";
5
6
  import { terminalOutputOptions } from "../render/terminal.mjs";
6
- import { ALL_SEARCH_TYPES, logger, parseSearchType, toCSV } from "../utils.mjs";
7
+ import { ALL_SEARCH_TYPES, logger, parseSearchType, setJsonOutput, toCSV } from "../utils.mjs";
7
8
  import { allTables } from "../local-store.mjs";
8
9
  import { createCommandContext, formatSiteResolution } from "../context.mjs";
9
10
  import { decideRoute, describeStop, liveNote, readRouteState, readSiteStates, resolveReadSite, stopAtRoute } from "../route.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";
@@ -215,6 +215,7 @@ const queryCommand = defineCommand({
215
215
  logger.error("Invalid --format. Use table, json, or csv.");
216
216
  process.exit(1);
217
217
  }
218
+ setJsonOutput(format === "json");
218
219
  if (args.sql || args.schema) {
219
220
  await runSqlMode({
220
221
  mode: args.schema ? { kind: "schema" } : {
@@ -341,7 +342,6 @@ const queryCommand = defineCommand({
341
342
  auth
342
343
  });
343
344
  const route = decideRoute(req, routeState);
344
- const json = format === "json";
345
345
  if (route.kind === "live") {
346
346
  const live = await connect();
347
347
  const siteUrl = site ?? await live.resolveSite(void 0);
@@ -406,7 +406,7 @@ const queryCommand = defineCommand({
406
406
  }, null, 2));
407
407
  return;
408
408
  }
409
- if (route.kind === "syncing" || route.kind === "prompt") stopAtRoute(route, req, routeState.auth, { json });
409
+ if (route.kind === "syncing" || route.kind === "prompt") stopAtRoute(route, req, routeState.auth);
410
410
  if (dataState || aggregationType) logger.warn("--data-state / --aggregation-type are ignored without --live");
411
411
  const siteUrl = site;
412
412
  if (!args.quiet) logger.debug(`Querying ${siteUrl} from local Parquet store...`);
@@ -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 });
@@ -588,7 +595,7 @@ async function runSqlMode(opts) {
588
595
  });
589
596
  if (route.kind === "syncing" || route.kind === "prompt") {
590
597
  views.close();
591
- stopAtRoute(route, req, routeState.auth, { json: opts.format === "json" });
598
+ stopAtRoute(route, req, routeState.auth);
592
599
  }
593
600
  }
594
601
  }
@@ -1,6 +1,6 @@
1
1
  import { reportCommandMeta } from "../command-meta.mjs";
2
2
  import { terminalOutputOptions } from "../render/terminal.mjs";
3
- import { logger, parseFetchBudget } from "../utils.mjs";
3
+ import { logger, parseFetchBudget, setJsonOutput } from "../utils.mjs";
4
4
  import { COMPARISON_FLAGS, PERIOD_FLAGS, parseWindowFlags, windowFlagErrorToException } from "../window.mjs";
5
5
  import { analysisNeeds, analyzerSources, analyzerTables, resolveAnalysisSource } from "../analysis-local.mjs";
6
6
  import { renderCliReport } from "../render/report.mjs";
@@ -127,6 +127,7 @@ function makeReportCommand(report) {
127
127
  ...reportArgs
128
128
  },
129
129
  async run({ args }) {
130
+ setJsonOutput(Boolean(args.json));
130
131
  const flags = windowFlags(args);
131
132
  const params = buildReportParams(report, args);
132
133
  const fetchBudget = parseFetchBudget(args["fetch-budget"]);
@@ -158,7 +159,6 @@ function makeReportCommand(report) {
158
159
  const { source, siteUrl, anchor, isLive } = await resolveAnalysisSource({
159
160
  site: args.site,
160
161
  live: !!args.live,
161
- json: !!args.json,
162
162
  label: `report ${report.id}`,
163
163
  types: provisional.map((step) => step.type),
164
164
  sources: reportSources(provisional),
@@ -1,8 +1,8 @@
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
- import { discoverLiveSitemap, loadSitemapUrls } from "../sitemap.mjs";
5
4
  import { HOSTED_ARGS, resolveHostedSite } from "../hosted-site.mjs";
5
+ import { discoverLiveSitemap, loadSitemapUrls } from "../sitemap.mjs";
6
6
  import process from "node:process";
7
7
  import { defineCommand } from "citty";
8
8
  import { fetchSitemap } from "gscdump/sites";
@@ -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
+ import { connectSiteStep, formatHostedSync, getHostedAccount, localModeRequired, resolveAuthentication, siteManagerOf } from "../auth-state.mjs";
1
2
  import { sitesCommandMeta } from "../command-meta.mjs";
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 ?? "?";
@@ -422,7 +422,7 @@ const LIST_ARGS = {
422
422
  }
423
423
  };
424
424
  async function runListHostedSites(args, authentication) {
425
- if (args["with-sitemaps"] || args["owner-only"]) throw new Error(`--with-sitemaps and --owner-only read Search Console permissions.\n${LOCAL_MODE_REQUIRED}`);
425
+ if (args["with-sitemaps"] || args["owner-only"]) throw localModeRequired("--with-sitemaps and --owner-only read Search Console permissions.");
426
426
  const { sites } = await getHostedAccount(authentication);
427
427
  if (args.json) {
428
428
  console.log(JSON.stringify(sites.map((site) => ({
@@ -438,10 +438,10 @@ async function runListHostedSites(args, authentication) {
438
438
  return;
439
439
  }
440
440
  if (sites.length === 0) {
441
- logger.warn("Your hosted record has no Sites. Connect a Site at https://gscdump.com/app/onboarding?step=connect-sites.");
441
+ logger.warn(`Your hosted record has no Sites. ${connectSiteStep(siteManagerOf(authentication))}`);
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/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 { SearchTypes } from "gscdump/query";
6
5
  import os from "node:os";
6
+ import { SearchTypes } from "gscdump/query";
7
7
  function setConfigDir(dir) {
8
8
  useCliRuntime().configDir = dir;
9
9
  }
package/dist/context.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { openQuotaLedger, quotaFetchOptions } from "./quota-ledger.mjs";
2
- import { LOCAL_MODE_REQUIRED, resolveAuthentication } from "./auth-state.mjs";
1
+ import { localModeRequired, resolveAuthentication } from "./auth-state.mjs";
3
2
  import { loadResolvedConfig } from "./config.mjs";
3
+ import { openQuotaLedger, quotaFetchOptions } from "./quota-ledger.mjs";
4
4
  import { resolveAuth } from "./auth.mjs";
5
5
  import { createLocalStore } from "./local-store.mjs";
6
6
  import { listStoreSites } from "./store-sites.mjs";
@@ -12,7 +12,7 @@ const FULL_SITE_URL_RE = /^(?:sc-domain:|https?:\/\/)/i;
12
12
  async function createCommandContext(opts = {}) {
13
13
  const { needsAuth = false, needsStore = false, interactive = false, byok, quota = true } = opts;
14
14
  const { config, dataDir } = await loadResolvedConfig();
15
- if (needsAuth && (await resolveAuthentication())._tag === "Hosted") throw new Error(LOCAL_MODE_REQUIRED);
15
+ if (needsAuth && (await resolveAuthentication())._tag === "Hosted") throw localModeRequired();
16
16
  const fetchOptions = needsAuth && quota ? quotaFetchOptions(await openQuotaLedger({ dataDir }), opts.fetchOptions) : opts.fetchOptions;
17
17
  const auth = needsAuth ? await resolveAuth({
18
18
  interactive,
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,11 +1,12 @@
1
+ import { stopDetailsOf } from "./stop.mjs";
1
2
  import { isUsageError } from "./command-registry.mjs";
3
+ import { describeRecordReadRefusal } from "./hosted-query.mjs";
2
4
  import { quotaStopOf } from "./quota-ledger.mjs";
5
+ import { isQueryError } from "gscdump/query";
3
6
  import { isAnalysisError } from "@gscdump/analysis/errors";
7
+ import { parseRecordReadRefusal } from "@gscdump/contracts";
4
8
  import { isEngineError } from "@gscdump/engine/errors";
5
9
  import { classifyError } from "gscdump/errors";
6
- import { isQueryError } from "gscdump/query";
7
- const HOSTED_KEY_REJECTED_REASON = "gscdump.com rejected the API key";
8
- const HOSTED_KEY_REJECTED = `${HOSTED_KEY_REJECTED_REASON}. Run \`gscdump auth login --mode hosted --api-key KEY\` with a valid key.`;
9
10
  const QUOTA_MESSAGE_RE = /quota|rate\s*limit/i;
10
11
  const ANSI_RE = /\x1B\[[0-9;]*m/g;
11
12
  function suggestionForGscError(error) {
@@ -103,19 +104,39 @@ const DEFECT_ERRORS = /* @__PURE__ */ new Set([
103
104
  "EvalError",
104
105
  "URIError"
105
106
  ]);
107
+ function recordReadRefusalOf(error) {
108
+ if (!(error instanceof Error) || error.name !== "GscdumpV1Error") return null;
109
+ return parseRecordReadRefusal(error.details);
110
+ }
111
+ function recordReadStop(refusal, text) {
112
+ return {
113
+ code: refusal.reason === "record_not_ready" ? "RECORD_NOT_READY" : "RANGE_NOT_SYNCED",
114
+ message: text.hint ? `${text.message} ${text.hint}` : text.message,
115
+ nextCommand: null
116
+ };
117
+ }
106
118
  function describeCliError(error) {
107
119
  if (isUsageError(error)) return {
108
120
  kind: "usage",
109
121
  message: error.message
110
122
  };
123
+ const stop = stopDetailsOf(error);
111
124
  if (error instanceof Error && error.routeStop !== void 0) return {
112
125
  kind: "stop",
113
- message: error.message
126
+ message: error.message,
127
+ ...stop ? { stop } : {}
114
128
  };
115
129
  if (error instanceof Error && quotaStopOf(error)) return {
116
130
  kind: "stop",
117
131
  message: error.message
118
132
  };
133
+ if (error instanceof Error && stop) return {
134
+ kind: "expected",
135
+ message: error.message,
136
+ hint: "",
137
+ showAuthSources: false,
138
+ stop
139
+ };
119
140
  if (error instanceof TypeError && error.message === "fetch failed") {
120
141
  const cause = error.cause;
121
142
  return {
@@ -136,13 +157,17 @@ function describeCliError(error) {
136
157
  hint: error.mode === "local" ? "Pass --live to run against the GSC API." : "",
137
158
  showAuthSources: false
138
159
  };
160
+ const refusal = recordReadRefusalOf(error);
161
+ if (refusal) {
162
+ const text = describeRecordReadRefusal(refusal);
163
+ return {
164
+ kind: "expected",
165
+ ...text,
166
+ showAuthSources: false,
167
+ stop: recordReadStop(refusal, text)
168
+ };
169
+ }
139
170
  const classified = classifyError(error);
140
- if (classified.message === HOSTED_KEY_REJECTED) return {
141
- kind: "expected",
142
- message: classified.message,
143
- hint: "",
144
- showAuthSources: false
145
- };
146
171
  return {
147
172
  kind: "expected",
148
173
  message: classified.message.replace(PARAM_NAME_RE, (name) => flagForParam(name)),
@@ -152,6 +177,7 @@ function describeCliError(error) {
152
177
  }
153
178
  async function reportCliError(error, options) {
154
179
  const report = describeCliError(error);
180
+ if (options.json && (report.kind === "stop" || report.kind === "expected") && report.stop) (options.writeStdout ?? ((value) => console.log(value)))(JSON.stringify({ error: report.stop }, null, 2));
155
181
  const red = (text) => options.color ? `\x1B[31m${text}\x1B[0m` : text;
156
182
  const lines = [];
157
183
  switch (report.kind) {
@@ -172,4 +198,4 @@ async function reportCliError(error, options) {
172
198
  const text = lines.join("\n");
173
199
  (options.write ?? ((value) => console.error(value)))(options.color ? text : text.replace(ANSI_RE, ""));
174
200
  }
175
- export { HOSTED_KEY_REJECTED, HOSTED_KEY_REJECTED_REASON, LocalStoreUnsupportedError, describeCliError, flagForParam, isAuthError, reportCliError };
201
+ export { LocalStoreUnsupportedError, describeCliError, flagForParam, isAuthError, reportCliError };
@@ -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,8 +1,7 @@
1
- import { getHostedAccount, hostedCredential, parseAuthentication, resolveAuthentication } from "./auth-state.mjs";
1
+ import { stopError } from "./stop.mjs";
2
+ import { connectSiteStep, getHostedAccount, hostedCredential, parseAuthentication, resolveAuthentication, siteManagerOf } from "./auth-state.mjs";
2
3
  import { loadConfig } from "./config.mjs";
3
4
  import { resolveCliEnvironment } from "./environment.mjs";
4
- import { logger } from "./utils.mjs";
5
- import process from "node:process";
6
5
  import { createGscdumpV1Client } from "@gscdump/sdk/v1";
7
6
  const HOSTED_ARGS = {
8
7
  "site": {
@@ -57,35 +56,56 @@ function matchHostedSite(sites, target) {
57
56
  sites: [...sites]
58
57
  };
59
58
  }
60
- function describeMatchFailure(match) {
61
- if (match.kind === "ambiguous") return `Multiple hosted Sites match "${match.target}": ${match.matches.map((site) => site.siteUrl).join(", ")}. Pass the exact Site URL with --site.`;
62
- if (match.sites.length === 0) return "Your hosted account has no Sites. Add a Site at https://gscdump.com.";
63
- const available = `Hosted Sites: ${match.sites.map((site) => site.siteUrl).join(", ")}.`;
64
- return match.target ? `No hosted Site matches "${match.target}". ${available}` : `Pass --site. ${available}`;
59
+ function noSitesStop(manager) {
60
+ return {
61
+ code: "NO_SITES",
62
+ message: `Your hosted record has no Sites. ${connectSiteStep(manager)} Then run the command again.`,
63
+ nextCommand: null
64
+ };
65
65
  }
66
- function fail(message) {
67
- logger.error(message);
68
- process.exit(1);
66
+ function siteMatchStop(match, manager) {
67
+ const nextCommand = "gscdump sites --json";
68
+ if (match.kind === "ambiguous") return {
69
+ code: "SITE_AMBIGUOUS",
70
+ message: `Multiple hosted Sites match "${match.target}": ${match.matches.map((site) => site.siteUrl).join(", ")}. Pass the exact Site URL with --site.`,
71
+ nextCommand
72
+ };
73
+ if (match.sites.length === 0) return noSitesStop(manager);
74
+ const available = `Hosted Sites: ${match.sites.map((site) => site.siteUrl).join(", ")}.`;
75
+ return match.target ? {
76
+ code: "SITE_NOT_FOUND",
77
+ message: `No hosted Site matches "${match.target}". ${available}`,
78
+ nextCommand
79
+ } : {
80
+ code: "SITE_REQUIRED",
81
+ message: `Pass --site. ${available}`,
82
+ nextCommand
83
+ };
69
84
  }
70
85
  async function resolveHostedSite(args, command) {
71
- if (args._?.length) fail(`\`gscdump ${command.name}\` does not accept a positional Site ID. Pass --site instead.`);
86
+ if (args._?.length) throw new Error(`\`gscdump ${command.name}\` does not accept a positional Site ID. Pass --site instead.`);
72
87
  const environment = resolveCliEnvironment().values;
73
88
  const authentication = args["api-key"] ? parseAuthentication({
74
89
  _tag: "Hosted",
75
90
  apiKey: args["api-key"],
76
91
  apiRoot: String(args["api-root"] || environment.GSCDUMP_API_ROOT || "https://gscdump.com/api")
77
92
  }) : await resolveAuthentication();
78
- if (authentication._tag !== "Hosted") fail(`\`gscdump ${command.name}\` reads the hosted record, so it needs Hosted mode. Run \`gscdump auth login --mode hosted\`, or pass --api-key. In Local mode, ${command.localAlternative}.`);
79
- if (args["api-root"] && String(args["api-root"]).replace(/\/+$/, "") !== authentication.apiRoot) fail("The API root changed. Pass --api-key for the new API root.");
93
+ if (authentication._tag !== "Hosted") throw stopError({
94
+ code: "HOSTED_MODE_REQUIRED",
95
+ message: `\`gscdump ${command.name}\` reads the hosted record, so it needs Hosted mode. Run \`gscdump auth login --mode hosted\`, or pass --api-key. In Local mode, ${command.localAlternative}.`,
96
+ nextCommand: "gscdump auth login --mode hosted"
97
+ });
98
+ if (args["api-root"] && String(args["api-root"]).replace(/\/+$/, "") !== authentication.apiRoot) throw new Error("The API root changed. Pass --api-key for the new API root.");
80
99
  const target = args.site ? String(args.site) : (await loadConfig()).defaultSite;
81
100
  const match = matchHostedSite((await getHostedAccount(authentication)).sites, target);
82
- if (match.kind !== "found") fail(describeMatchFailure(match));
101
+ if (match.kind !== "found") throw stopError(siteMatchStop(match, siteManagerOf(authentication)));
83
102
  return {
84
103
  client: createGscdumpV1Client({
85
104
  apiRoot: authentication.apiRoot,
86
105
  credential: hostedCredential(authentication)
87
106
  }),
88
- site: match.site
107
+ site: match.site,
108
+ authentication
89
109
  };
90
110
  }
91
- export { HOSTED_ARGS, matchHostedSite, resolveHostedSite };
111
+ export { HOSTED_ARGS, matchHostedSite, noSitesStop, resolveHostedSite, siteMatchStop };
@@ -1,9 +1,9 @@
1
1
  import { resolveBYOK, resolveServiceAccount } from "../auth.mjs";
2
2
  import { COMPARISON_FLAGS, PERIOD_FLAGS } from "../window.mjs";
3
+ import { isQueryError } from "gscdump/query";
3
4
  import { isAnalysisError } from "@gscdump/analysis/errors";
4
5
  import { isEngineError } from "@gscdump/engine/errors";
5
6
  import { classifyError } from "gscdump/errors";
6
- import { isQueryError } from "gscdump/query";
7
7
  const PERIODS = PERIOD_FLAGS;
8
8
  const COMPARISONS = COMPARISON_FLAGS;
9
9
  const mcpHandlerErrors = {
@@ -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.8.0";
1
+ var version = "5.1.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)
package/dist/route.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { stopError } from "./stop.mjs";
1
2
  import { probeAuth } from "./auth.mjs";
2
3
  import { listStoreSites } from "./store-sites.mjs";
3
4
  import { formatSiteResolution, siteArg } from "./context.mjs";
@@ -199,11 +200,8 @@ function stopCode(route) {
199
200
  case "partial": return "STORE_RANGE_NOT_COVERED";
200
201
  }
201
202
  }
202
- function routeStopError(route, message) {
203
- return Object.assign(new Error(message), {
204
- name: "RouteStopError",
205
- routeStop: route
206
- });
203
+ function routeStopError(route, details) {
204
+ return Object.assign(stopError(details, "RouteStopError"), { routeStop: route });
207
205
  }
208
206
  async function needCoverage(needs, states) {
209
207
  const reader = syncStateCoverageReader(states);
@@ -299,9 +297,7 @@ function describeStop(route, req, auth) {
299
297
  } } : {}
300
298
  };
301
299
  }
302
- function stopAtRoute(route, req, auth, opts) {
303
- const details = describeStop(route, req, auth);
304
- if (opts.json) console.log(JSON.stringify({ error: details }, null, 2));
305
- throw routeStopError(route, details.message);
300
+ function stopAtRoute(route, req, auth) {
301
+ throw routeStopError(route, describeStop(route, req, auth));
306
302
  }
307
303
  export { decideRoute, describeStop, liveCommand, liveNote, needCoverage, readRouteState, readSiteStates, resolveReadSite, routeMessage, routeStopError, stopAtRoute, stopCode, syncCommandFor };
@@ -7,6 +7,8 @@ export interface CliRuntime {
7
7
  configDir: string;
8
8
  configDirOverridden: boolean;
9
9
  environment: CliEnvironmentSource;
10
+ /** The command writes JSON to stdout. A stop then prints its JSON form there too. */
11
+ jsonOutput: boolean;
10
12
  logger: ConsolaInstance;
11
13
  quiet: boolean;
12
14
  rawArgs: string[];
package/dist/runtime.mjs CHANGED
@@ -21,6 +21,7 @@ function createCliRuntime(opts = {}) {
21
21
  configDir: opts.configDir ?? path.join(os.homedir(), ".config", "gscdump"),
22
22
  configDirOverridden: false,
23
23
  environment: opts.environment ?? process.env,
24
+ jsonOutput: false,
24
25
  logger: baseLogger.withTag("gscdump"),
25
26
  quiet: false,
26
27
  rawArgs: [...opts.rawArgs ?? process.argv.slice(2)]
package/dist/stop.mjs ADDED
@@ -0,0 +1,10 @@
1
+ function stopError(details, name = "StopError") {
2
+ return Object.assign(new Error(details.message), {
3
+ name,
4
+ stopDetails: details
5
+ });
6
+ }
7
+ function stopDetailsOf(error) {
8
+ return error instanceof Error ? error.stopDetails : void 0;
9
+ }
10
+ export { stopDetailsOf, stopError };
@@ -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
@@ -2,8 +2,8 @@ import { useCliRuntime } from "./runtime.mjs";
2
2
  import { version } from "./package.mjs";
3
3
  import process from "node:process";
4
4
  import fs from "node:fs/promises";
5
- import { SearchTypes } from "gscdump/query";
6
5
  import os from "node:os";
6
+ import { SearchTypes } from "gscdump/query";
7
7
  import { Buffer } from "node:buffer";
8
8
  import { MAX_FETCH_BUDGET } from "@gscdump/engine/analysis-types";
9
9
  const ALL_SEARCH_TYPES = Object.values(SearchTypes);
@@ -40,10 +40,14 @@ const OUTPUT_ARGS = {
40
40
  description: "Suppress info/success output"
41
41
  }
42
42
  };
43
+ function setJsonOutput(json) {
44
+ useCliRuntime().jsonOutput = json;
45
+ }
43
46
  function applyOutputMode(args) {
44
47
  const json = Boolean(args.json);
45
48
  const quiet = json || Boolean(args.quiet);
46
49
  setQuiet(quiet);
50
+ setJsonOutput(json);
47
51
  return {
48
52
  json,
49
53
  quiet
@@ -148,14 +152,14 @@ function clearLine() {
148
152
  process.stdout.write("\r\x1B[K");
149
153
  }
150
154
  function displayPath(absPath) {
151
- const cwd = process.cwd();
152
155
  const home = os.homedir();
153
- if (absPath === cwd) return ".";
154
- if (absPath.startsWith(`${cwd}/`)) return absPath.slice(cwd.length + 1);
155
156
  if (absPath === home) return "~";
156
157
  if (absPath.startsWith(`${home}/`)) return `~/${absPath.slice(home.length + 1)}`;
157
158
  return absPath;
158
159
  }
160
+ function plural(count, one, many) {
161
+ return `${count.toLocaleString("en-US")} ${count === 1 ? one : many}`;
162
+ }
159
163
  function formatAge(ms) {
160
164
  const delta = Date.now() - ms;
161
165
  if (delta < 6e4) return "just now";
@@ -193,4 +197,4 @@ async function readUrlList(input) {
193
197
  for await (const chunk of process.stdin) chunks.push(chunk);
194
198
  return parseUrlLines(Buffer.concat(chunks).toString("utf-8"));
195
199
  }
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 };
200
+ 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, setJsonOutput, setNoColor, setQuiet, showSplash, toCSV, withConfiguredOutput };