@gscdump/cli 0.8.2 → 0.9.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,6 +1,6 @@
1
1
  import { n as __require, t as __exportAll } from "../rolldown-runtime.mjs";
2
- import http from "node:http";
3
2
  import { basename } from "node:path";
3
+ import http from "node:http";
4
4
  import https from "node:https";
5
5
  import st from "node:zlib";
6
6
  import me, { PassThrough, pipeline } from "node:stream";
package/dist/index.mjs CHANGED
@@ -1,21 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as __exportAll } from "./_chunks/rolldown-runtime.mjs";
3
- import { t as ofetch } from "./_chunks/libs/ofetch.mjs";
4
3
  import { a as loadConfig, c as setConfigDir, i as getConfigPath, n as defaultDataDir, o as resolveDataDir, r as getConfigDir, s as saveConfig } from "./_chunks/config.mjs";
4
+ import { t as ofetch } from "./_chunks/libs/ofetch.mjs";
5
5
  import process from "node:process";
6
6
  import { defineCommand, runMain } from "citty";
7
7
  import { defaultAnalyzerRegistry } from "@gscdump/analysis/registry";
8
+ import fs, { readFile, readdir, rm } from "node:fs/promises";
9
+ import path, { join } from "node:path";
8
10
  import { AnalyzerCapabilityError, analyzeFromSource, createEngineQuerySource } from "@gscdump/analysis";
9
11
  import { createGscApiQuerySource } from "@gscdump/engine-gsc-api";
12
+ import { decodeSiteId, normalizeSiteUrl } from "gscdump/tenant";
13
+ import os from "node:os";
10
14
  import { cancel, confirm, isCancel, multiselect, select, text } from "@clack/prompts";
11
15
  import { addSite, batchInspectUrls, batchRequestIndexing, createAuth, daysAgo, deleteSite, discoverSitemap, fetchSitemap, fetchSitemapUrls, fetchSitesWithSitemaps, formatErrorForCli, getDateRange, getIndexingMetadata, getVerificationToken, getVerifiedSite, googleSearchConsole, listVerifiedSites, progressBar, requestIndexing, runSequentialBatch, siteUrlToVerificationSite, unverifySite, verificationMethodsFor, verifySite } from "gscdump";
12
- import fs, { readFile, rm } from "node:fs/promises";
13
16
  import { createServer } from "node:http";
14
- import path from "node:path";
15
17
  import { JWT, OAuth2Client } from "google-auth-library";
16
18
  import { Buffer } from "node:buffer";
17
19
  import fs$1 from "node:fs";
18
- import os from "node:os";
19
20
  import { createConsola } from "consola";
20
21
  import { createNodeHarness } from "@gscdump/engine-duckdb-node";
21
22
  import { TABLE_DIMS, transformGscRow } from "@gscdump/engine/ingest";
@@ -26,48 +27,11 @@ import { createEmptyTypesStore, createIndexingMetadataStore, createInspectionSto
26
27
  import { createGscMcpServer } from "@gscdump/mcp/server";
27
28
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
28
29
  import { SearchTypes, and, between, contains, country, date, device, eq, gsc, notRegex, page, query, regex, searchAppearance } from "gscdump/query";
30
+ import { defaultReportRegistry, dryRunReport, formatReport, runReport } from "@gscdump/analysis/report";
31
+ import { resolveWindow } from "@gscdump/engine/period";
29
32
  import { inferLegacyTier } from "@gscdump/engine";
30
33
  import { DEFAULT_ROLLUPS, rebuildRollups } from "@gscdump/analysis/rollups";
31
34
  import { filesystemStats } from "@gscdump/engine/filesystem";
32
- var LocalStoreUnsupportedError = class extends Error {
33
- constructor(tool) {
34
- super(`analysis "${tool}" is not yet implemented against the local Parquet store`);
35
- this.name = "LocalStoreUnsupportedError";
36
- }
37
- };
38
- var LocalStoreEmptyError = class extends Error {
39
- constructor(siteUrl) {
40
- super(`no local data synced for ${siteUrl} (run \`gscdump sync\` first)`);
41
- this.name = "LocalStoreEmptyError";
42
- }
43
- };
44
- async function hasLocalData(store, siteUrl) {
45
- return (await store.engine.listLive({
46
- userId: store.userId,
47
- siteId: store.siteIdFor(siteUrl)
48
- })).length > 0;
49
- }
50
- async function runLocalAnalysis(store, siteUrl, params) {
51
- return analyzeFromSource(createEngineQuerySource({
52
- engine: store.engine,
53
- ctx: {
54
- userId: store.userId,
55
- siteId: store.siteIdFor(siteUrl)
56
- }
57
- }), params, defaultAnalyzerRegistry).catch((e) => {
58
- if (e instanceof AnalyzerCapabilityError) throw new LocalStoreUnsupportedError(params.type);
59
- throw e;
60
- });
61
- }
62
- async function runLiveAnalysis(client, siteUrl, params) {
63
- return analyzeFromSource(createGscApiQuerySource({
64
- client,
65
- siteUrl
66
- }), params, defaultAnalyzerRegistry).catch((e) => {
67
- if (e instanceof AnalyzerCapabilityError) throw new LocalStoreUnsupportedError(params.type);
68
- throw e;
69
- });
70
- }
71
35
  const ENV_LINE_RE$1 = /^([^=]+)=(.*)$/;
72
36
  function parseEnvFile(envPath) {
73
37
  let content;
@@ -110,7 +74,7 @@ function loadEnvFromCwd() {
110
74
  }
111
75
  return applied;
112
76
  }
113
- const VERSION = "0.8.2";
77
+ const VERSION = "0.9.0";
114
78
  const baseLogger = createConsola({
115
79
  stdout: process.stderr,
116
80
  stderr: process.stderr
@@ -735,6 +699,92 @@ async function gscErrorHandler(error) {
735
699
  console.error();
736
700
  process.exit(1);
737
701
  }
702
+ var LocalStoreUnsupportedError = class extends Error {
703
+ constructor(tool) {
704
+ super(`analysis "${tool}" is not yet implemented against the local Parquet store`);
705
+ this.name = "LocalStoreUnsupportedError";
706
+ }
707
+ };
708
+ async function hasLocalData(store, siteUrl) {
709
+ return (await store.engine.listLive({
710
+ userId: store.userId,
711
+ siteId: store.siteIdFor(siteUrl)
712
+ })).length > 0;
713
+ }
714
+ async function listLocalSites(dataDir, userId = "local") {
715
+ return readdir(join(dataDir, `u_${userId}`), { withFileTypes: true }).then((entries) => entries.filter((e) => e.isDirectory() && (e.name.startsWith("d_") || e.name.startsWith("h_"))).map((e) => decodeSiteId(e.name))).catch(() => []);
716
+ }
717
+ function pickLocalSite(siteUrls, hint) {
718
+ if (siteUrls.length === 0) return null;
719
+ if (!hint) return siteUrls.length === 1 ? siteUrls[0] : null;
720
+ const normalized = normalizeSiteUrl(hint);
721
+ const exact = siteUrls.find((s) => s === normalized || s === hint);
722
+ if (exact) return exact;
723
+ return siteUrls.find((s) => s.includes(hint) || hint.includes(s)) ?? null;
724
+ }
725
+ async function resolveAnalysisSource(args) {
726
+ const isLive = !!args.live;
727
+ const format = args.json ? "json" : args.format ? String(args.format) : "table";
728
+ if (!isLive) {
729
+ const config = await loadConfig();
730
+ const dataDir = resolveDataDir(config);
731
+ const store = createLocalStore({ dataDir });
732
+ const siteHint = args.site ? String(args.site) : config.defaultSite;
733
+ const localSites = await listLocalSites(dataDir, store.userId);
734
+ const siteUrl = pickLocalSite(localSites, siteHint);
735
+ if (!siteUrl) {
736
+ if (localSites.length === 0) logger.error(`No local data found in ${dataDir}. Run \`gscdump sync\` first, or pass --live.`);
737
+ else logger.error(`Could not resolve site${siteHint ? ` from "${siteHint}"` : ""}. Local sites: ${localSites.join(", ")}`);
738
+ process.exit(1);
739
+ }
740
+ if (!await hasLocalData(store, siteUrl).catch(() => false)) {
741
+ logger.error(`No local data for ${siteUrl}. Run \`gscdump sync\` first, or pass --live.`);
742
+ process.exit(1);
743
+ }
744
+ const source = createEngineQuerySource({
745
+ engine: store.engine,
746
+ ctx: {
747
+ userId: store.userId,
748
+ siteId: store.siteIdFor(siteUrl)
749
+ }
750
+ });
751
+ const runAnalysis = (params) => analyzeFromSource(source, params, defaultAnalyzerRegistry).catch((e) => {
752
+ if (e instanceof AnalyzerCapabilityError) {
753
+ logger.error(`${new LocalStoreUnsupportedError(params.type).message}. Pass --live to run against the GSC API.`);
754
+ process.exit(1);
755
+ }
756
+ logger.error(`Local analysis failed: ${e.message}`);
757
+ process.exit(1);
758
+ });
759
+ return {
760
+ source,
761
+ siteUrl,
762
+ format,
763
+ isLive,
764
+ runAnalysis
765
+ };
766
+ }
767
+ const ctx = await createCommandContext({
768
+ needsAuth: true,
769
+ needsStore: false
770
+ });
771
+ const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
772
+ const source = createGscApiQuerySource({
773
+ client: ctx.client,
774
+ siteUrl
775
+ });
776
+ const runAnalysis = (params) => analyzeFromSource(source, params, defaultAnalyzerRegistry).catch((e) => {
777
+ if (e instanceof AnalyzerCapabilityError) throw new LocalStoreUnsupportedError(params.type);
778
+ return gscErrorHandler(e);
779
+ });
780
+ return {
781
+ source,
782
+ siteUrl,
783
+ format,
784
+ isLive,
785
+ runAnalysis
786
+ };
787
+ }
738
788
  const ANALYSIS_TOOLS = defaultAnalyzerRegistry.listAnalyzerIds();
739
789
  const TOOL_EXTRA_ARGS = {
740
790
  brand: { "brand-terms": {
@@ -851,40 +901,14 @@ function makeToolCommand(tool) {
851
901
  ...extraArgs
852
902
  },
853
903
  async run({ args }) {
854
- const ctx = await createCommandContext({
855
- needsAuth: true,
856
- needsStore: !args.live
904
+ const { format, runAnalysis } = await resolveAnalysisSource({
905
+ site: args.site,
906
+ live: !!args.live,
907
+ json: !!args.json,
908
+ format: args.format
857
909
  });
858
- const siteUrl = await ctx.resolveSite(args.site);
859
910
  logger.info(`Running ${tool} analysis...`);
860
- const params = buildParams(tool, args);
861
- const format = args.json ? "json" : String(args.format);
862
- if (!args.live) {
863
- const store = ctx.store;
864
- if (!await hasLocalData(store, siteUrl).catch(() => false)) {
865
- logger.error(`No local data for ${siteUrl}. Run \`gscdump sync\` first, or pass --live.`);
866
- process.exit(1);
867
- }
868
- const localResult = await runLocalAnalysis(store, siteUrl, params).catch((e) => {
869
- if (e instanceof LocalStoreUnsupportedError) {
870
- logger.error(`${e.message}. Pass --live to run against the GSC API.`);
871
- process.exit(1);
872
- }
873
- if (e instanceof LocalStoreEmptyError) {
874
- logger.error(`${e.message}`);
875
- process.exit(1);
876
- }
877
- logger.error(`Local analysis failed: ${e.message}`);
878
- process.exit(1);
879
- });
880
- if (format === "json") {
881
- console.log(JSON.stringify(localResult, null, 2));
882
- return;
883
- }
884
- renderResults(localResult.results, localResult.results.length, format);
885
- return;
886
- }
887
- const result = await runLiveAnalysis(ctx.client, siteUrl, params).catch(gscErrorHandler);
911
+ const result = await runAnalysis(buildParams(tool, args));
888
912
  if (format === "json") {
889
913
  console.log(JSON.stringify(result, null, 2));
890
914
  return;
@@ -3966,6 +3990,222 @@ async function writeOutput(opts) {
3966
3990
  function isKnownTable$1(name) {
3967
3991
  return allTables().includes(name);
3968
3992
  }
3993
+ const REPORT_IDS = defaultReportRegistry.listReportIds();
3994
+ const PERIOD_ALIASES = {
3995
+ "7d": "last-7d",
3996
+ "28d": "last-28d",
3997
+ "30d": "last-30d",
3998
+ "90d": "last-90d",
3999
+ "180d": "last-180d",
4000
+ "365d": "last-365d",
4001
+ "last-7d": "last-7d",
4002
+ "last-28d": "last-28d",
4003
+ "last-30d": "last-30d",
4004
+ "last-90d": "last-90d",
4005
+ "last-180d": "last-180d",
4006
+ "last-365d": "last-365d",
4007
+ "mtd": "mtd",
4008
+ "ytd": "ytd",
4009
+ "custom": "custom"
4010
+ };
4011
+ const COMPARISON_ALIASES = {
4012
+ "none": "none",
4013
+ "prev": "prev-period",
4014
+ "prev-period": "prev-period",
4015
+ "prior": "prev-period",
4016
+ "prior-period": "prev-period",
4017
+ "yoy": "yoy"
4018
+ };
4019
+ function resolvePeriod(input, fallback) {
4020
+ if (!input) return fallback;
4021
+ const preset = PERIOD_ALIASES[input.toLowerCase()];
4022
+ if (!preset) throw new Error(`Unknown --period "${input}". Supported: 7d, 28d, 30d, 90d, 180d, 365d, mtd, ytd, custom.`);
4023
+ return preset;
4024
+ }
4025
+ function resolveComparison(input, fallback) {
4026
+ if (!input) return fallback;
4027
+ const mode = COMPARISON_ALIASES[input.toLowerCase()];
4028
+ if (!mode) throw new Error(`Unknown --vs "${input}". Supported: none, prev-period, yoy.`);
4029
+ return mode;
4030
+ }
4031
+ function reportArgsToCitty(spec) {
4032
+ const out = {};
4033
+ for (const [key, def] of Object.entries(spec)) out[key] = {
4034
+ type: def.type === "boolean" ? "boolean" : "string",
4035
+ description: def.description,
4036
+ default: def.default == null ? void 0 : String(def.default),
4037
+ alias: def.alias,
4038
+ required: def.required
4039
+ };
4040
+ return out;
4041
+ }
4042
+ function buildReportParams(report, args) {
4043
+ const params = {};
4044
+ for (const [key, def] of Object.entries(report.argsSpec)) {
4045
+ const raw = args[key];
4046
+ if (raw == null || raw === "") continue;
4047
+ if (def.type === "number") {
4048
+ const n = Number(raw);
4049
+ if (Number.isFinite(n)) params[toCamel(key)] = n;
4050
+ } else if (def.type === "boolean") params[toCamel(key)] = !!raw;
4051
+ else params[toCamel(key)] = raw;
4052
+ }
4053
+ return params;
4054
+ }
4055
+ function toCamel(kebab) {
4056
+ return kebab.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
4057
+ }
4058
+ function makeReportCommand(report) {
4059
+ const reportArgs = reportArgsToCitty(report.argsSpec);
4060
+ return defineCommand({
4061
+ meta: {
4062
+ name: report.id,
4063
+ description: report.description
4064
+ },
4065
+ args: {
4066
+ "site": {
4067
+ type: "string",
4068
+ alias: "s",
4069
+ description: "Site URL"
4070
+ },
4071
+ "period": {
4072
+ type: "string",
4073
+ description: "Window: 7d|28d|90d|mtd|ytd|custom",
4074
+ default: presetToFlag(report.defaultPeriod)
4075
+ },
4076
+ "vs": {
4077
+ type: "string",
4078
+ description: "Comparison: none|prev-period|yoy",
4079
+ default: report.defaultComparison
4080
+ },
4081
+ "start": {
4082
+ type: "string",
4083
+ description: "Custom start date (YYYY-MM-DD)"
4084
+ },
4085
+ "end": {
4086
+ type: "string",
4087
+ description: "Custom end date (YYYY-MM-DD)"
4088
+ },
4089
+ "prev-start": {
4090
+ type: "string",
4091
+ description: "Override comparison start"
4092
+ },
4093
+ "prev-end": {
4094
+ type: "string",
4095
+ description: "Override comparison end"
4096
+ },
4097
+ "live": {
4098
+ type: "boolean",
4099
+ default: false,
4100
+ description: "Force live GSC API; bypass local store"
4101
+ },
4102
+ "json": {
4103
+ type: "boolean",
4104
+ default: false,
4105
+ description: "Emit full ReportResult JSON"
4106
+ },
4107
+ "explain": {
4108
+ type: "boolean",
4109
+ default: false,
4110
+ description: "Print plan steps + window without executing"
4111
+ },
4112
+ "dry-run": {
4113
+ type: "boolean",
4114
+ default: false,
4115
+ description: "Alias for --explain"
4116
+ },
4117
+ ...reportArgs
4118
+ },
4119
+ async run({ args }) {
4120
+ const preset = resolvePeriod(args.period, report.defaultPeriod);
4121
+ const comparison = resolveComparison(args.vs, report.defaultComparison);
4122
+ const window = resolveWindow({
4123
+ preset,
4124
+ comparison,
4125
+ start: args.start,
4126
+ end: args.end
4127
+ });
4128
+ if (args["prev-start"] && args["prev-end"]) window.comparison = {
4129
+ start: String(args["prev-start"]),
4130
+ end: String(args["prev-end"])
4131
+ };
4132
+ const params = buildReportParams(report, args);
4133
+ if (args.explain || args["dry-run"]) {
4134
+ const dry = await dryRunReport(report, {
4135
+ site: args.site ? String(args.site) : "(unresolved)",
4136
+ window,
4137
+ params,
4138
+ registryVersion: defaultReportRegistry.version
4139
+ });
4140
+ console.log(JSON.stringify({
4141
+ id: report.id,
4142
+ window,
4143
+ comparison,
4144
+ plan: dry.steps
4145
+ }, null, 2));
4146
+ return;
4147
+ }
4148
+ const { source, siteUrl } = await resolveAnalysisSource({
4149
+ site: args.site,
4150
+ live: !!args.live,
4151
+ json: !!args.json
4152
+ });
4153
+ const result = await runReport(report, {
4154
+ source,
4155
+ analyzers: defaultAnalyzerRegistry,
4156
+ ctx: {
4157
+ site: siteUrl,
4158
+ window,
4159
+ params,
4160
+ registryVersion: defaultReportRegistry.version
4161
+ }
4162
+ });
4163
+ if (args.json) {
4164
+ console.log(JSON.stringify(result, null, 2));
4165
+ return;
4166
+ }
4167
+ console.log(formatReport(result));
4168
+ if (result.meta.degraded) logger.warn(`degraded: ${result.meta.steps.filter((s) => s.status === "error").map((s) => `${s.key}(${s.error})`).join(", ")}`);
4169
+ }
4170
+ });
4171
+ }
4172
+ function presetToFlag(preset) {
4173
+ if (preset === "mtd" || preset === "ytd" || preset === "custom") return preset;
4174
+ return preset.replace(/^last-/, "");
4175
+ }
4176
+ const reportCommand = defineCommand({
4177
+ meta: {
4178
+ name: "report",
4179
+ description: "Run an intent-keyed report (composes analyzers into bounded sections)"
4180
+ },
4181
+ subCommands: {
4182
+ list: defineCommand({
4183
+ meta: {
4184
+ name: "list",
4185
+ description: "List available report ids"
4186
+ },
4187
+ args: { json: {
4188
+ type: "boolean",
4189
+ default: false,
4190
+ description: "Output as JSON"
4191
+ } },
4192
+ async run({ args }) {
4193
+ const reports = defaultReportRegistry.listReports().map((r) => ({
4194
+ id: r.id,
4195
+ description: r.description,
4196
+ defaultPeriod: r.defaultPeriod,
4197
+ defaultComparison: r.defaultComparison
4198
+ }));
4199
+ if (args.json) {
4200
+ console.log(JSON.stringify(reports, null, 2));
4201
+ return;
4202
+ }
4203
+ for (const r of reports) console.log(`${r.id.padEnd(16)} ${r.description}`);
4204
+ }
4205
+ }),
4206
+ ...Object.fromEntries(REPORT_IDS.map((id) => [id, makeReportCommand(defaultReportRegistry.getReport(id))]))
4207
+ }
4208
+ });
3969
4209
  const sitemapsCommand = defineCommand({
3970
4210
  meta: {
3971
4211
  name: "sitemaps",
@@ -5819,6 +6059,7 @@ runMain(defineCommand({
5819
6059
  indexing: indexingCommand,
5820
6060
  entities: entitiesCommand,
5821
6061
  analyze: analyzeCommand,
6062
+ report: reportCommand,
5822
6063
  auth: authCommand,
5823
6064
  config: configCommand,
5824
6065
  profile: profileCommand,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/cli",
3
3
  "type": "module",
4
- "version": "0.8.2",
4
+ "version": "0.9.0",
5
5
  "description": "CLI for Google Search Console - dump, query, and run MCP server",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -41,12 +41,12 @@
41
41
  "consola": "^3.4.2",
42
42
  "google-auth-library": "^10.6.2",
43
43
  "open": "^11.0.0",
44
- "@gscdump/engine-duckdb-node": "0.8.2",
45
- "@gscdump/engine": "0.8.2",
46
- "@gscdump/engine-gsc-api": "0.8.2",
47
- "@gscdump/mcp": "0.8.2",
48
- "@gscdump/analysis": "0.8.2",
49
- "gscdump": "0.8.2"
44
+ "@gscdump/engine": "0.9.0",
45
+ "@gscdump/engine-duckdb-node": "0.9.0",
46
+ "@gscdump/analysis": "0.9.0",
47
+ "@gscdump/engine-gsc-api": "0.9.0",
48
+ "@gscdump/mcp": "0.9.0",
49
+ "gscdump": "0.9.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@duckdb/node-api": "1.5.1-r.2",