@gscdump/cli 0.8.1 → 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.
package/dist/index.mjs CHANGED
@@ -1,17 +1,18 @@
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";
5
- import os from "node:os";
6
- import path from "node:path";
4
+ import { t as ofetch } from "./_chunks/libs/ofetch.mjs";
7
5
  import process from "node:process";
8
6
  import { defineCommand, runMain } from "citty";
9
7
  import { defaultAnalyzerRegistry } from "@gscdump/analysis/registry";
8
+ import fs, { readFile, readdir, rm } from "node:fs/promises";
9
+ import path, { join } from "node:path";
10
10
  import { AnalyzerCapabilityError, analyzeFromSource, createEngineQuerySource } from "@gscdump/analysis";
11
11
  import { createGscApiQuerySource } from "@gscdump/engine-gsc-api";
12
+ import { decodeSiteId, normalizeSiteUrl } from "gscdump/tenant";
13
+ import os from "node:os";
12
14
  import { cancel, confirm, isCancel, multiselect, select, text } from "@clack/prompts";
13
- import { addSite, batchInspectUrls, batchRequestIndexing, createAuth, daysAgo, deleteSite, fetchSitemap, fetchSitesWithSitemaps, formatErrorForCli, getDateRange, getIndexingMetadata, getVerificationToken, googleSearchConsole, progressBar, requestIndexing, siteUrlToVerificationSite, verificationMethodsFor, verifySite } from "gscdump";
14
- import fs, { readFile, rm } from "node:fs/promises";
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";
15
16
  import { createServer } from "node:http";
16
17
  import { JWT, OAuth2Client } from "google-auth-library";
17
18
  import { Buffer } from "node:buffer";
@@ -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.1";
77
+ const VERSION = "0.9.0";
114
78
  const baseLogger = createConsola({
115
79
  stdout: process.stderr,
116
80
  stderr: process.stderr
@@ -119,6 +83,28 @@ const logger = baseLogger.withTag("gscdump");
119
83
  function setQuiet(quiet) {
120
84
  if (quiet) baseLogger.level = 1;
121
85
  }
86
+ const OUTPUT_ARGS = {
87
+ json: {
88
+ type: "boolean",
89
+ default: false,
90
+ description: "Output as JSON"
91
+ },
92
+ quiet: {
93
+ type: "boolean",
94
+ alias: "q",
95
+ default: false,
96
+ description: "Suppress info/success output"
97
+ }
98
+ };
99
+ function applyOutputMode(args) {
100
+ const json = Boolean(args.json);
101
+ const quiet = json || Boolean(args.quiet);
102
+ setQuiet(quiet);
103
+ return {
104
+ json,
105
+ quiet
106
+ };
107
+ }
122
108
  let colorEnabled = (() => {
123
109
  if (process.env.NO_COLOR) return false;
124
110
  if (process.argv.includes("--no-color")) return false;
@@ -257,7 +243,8 @@ async function loadServiceAccount(jsonPath) {
257
243
  });
258
244
  }
259
245
  async function resolveServiceAccount(opts = {}) {
260
- const p = opts.path || process.env.GSC_SERVICE_ACCOUNT_JSON || process.env.GOOGLE_APPLICATION_CREDENTIALS;
246
+ let p = opts.path || process.env.GSC_SERVICE_ACCOUNT_JSON || process.env.GOOGLE_APPLICATION_CREDENTIALS;
247
+ if (!p) p = (await loadConfig().catch(() => null))?.serviceAccountPath;
261
248
  if (!p) return null;
262
249
  return loadServiceAccount(p);
263
250
  }
@@ -538,15 +525,21 @@ function redactCred(v, keepTail = 6) {
538
525
  async function describeAuthProvenance() {
539
526
  const rows = [];
540
527
  const warnings = [];
541
- const saPath = process.env.GSC_SERVICE_ACCOUNT_JSON || process.env.GOOGLE_APPLICATION_CREDENTIALS;
542
- if (saPath) {
528
+ const saEnvPath = process.env.GSC_SERVICE_ACCOUNT_JSON || process.env.GOOGLE_APPLICATION_CREDENTIALS;
529
+ const saConfigPath = !saEnvPath ? (await loadConfig().catch(() => null))?.serviceAccountPath : void 0;
530
+ const saPath = saEnvPath || saConfigPath;
531
+ if (saEnvPath) {
543
532
  const saEnv = process.env.GSC_SERVICE_ACCOUNT_JSON ? "GSC_SERVICE_ACCOUNT_JSON" : "GOOGLE_APPLICATION_CREDENTIALS";
544
533
  rows.push({
545
534
  field: "service_account",
546
535
  source: envSourceLabel(saEnv),
547
- value: displayPath(saPath)
536
+ value: displayPath(saEnvPath)
548
537
  });
549
- }
538
+ } else if (saConfigPath) rows.push({
539
+ field: "service_account",
540
+ source: `${displayPath(`${getConfigDir()}/config.json`)}`,
541
+ value: displayPath(saConfigPath)
542
+ });
550
543
  const clientId = pickEnvSource("GSC_CLIENT_ID", "GOOGLE_CLIENT_ID");
551
544
  const clientSecret = pickEnvSource("GSC_CLIENT_SECRET", "GOOGLE_CLIENT_SECRET");
552
545
  const config = await loadConfig().catch(() => null);
@@ -706,6 +699,92 @@ async function gscErrorHandler(error) {
706
699
  console.error();
707
700
  process.exit(1);
708
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
+ }
709
788
  const ANALYSIS_TOOLS = defaultAnalyzerRegistry.listAnalyzerIds();
710
789
  const TOOL_EXTRA_ARGS = {
711
790
  brand: { "brand-terms": {
@@ -822,40 +901,14 @@ function makeToolCommand(tool) {
822
901
  ...extraArgs
823
902
  },
824
903
  async run({ args }) {
825
- const ctx = await createCommandContext({
826
- needsAuth: true,
827
- 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
828
909
  });
829
- const siteUrl = await ctx.resolveSite(args.site);
830
910
  logger.info(`Running ${tool} analysis...`);
831
- const params = buildParams(tool, args);
832
- const format = args.json ? "json" : String(args.format);
833
- if (!args.live) {
834
- const store = ctx.store;
835
- if (!await hasLocalData(store, siteUrl).catch(() => false)) {
836
- logger.error(`No local data for ${siteUrl}. Run \`gscdump sync\` first, or pass --live.`);
837
- process.exit(1);
838
- }
839
- const localResult = await runLocalAnalysis(store, siteUrl, params).catch((e) => {
840
- if (e instanceof LocalStoreUnsupportedError) {
841
- logger.error(`${e.message}. Pass --live to run against the GSC API.`);
842
- process.exit(1);
843
- }
844
- if (e instanceof LocalStoreEmptyError) {
845
- logger.error(`${e.message}`);
846
- process.exit(1);
847
- }
848
- logger.error(`Local analysis failed: ${e.message}`);
849
- process.exit(1);
850
- });
851
- if (format === "json") {
852
- console.log(JSON.stringify(localResult, null, 2));
853
- return;
854
- }
855
- renderResults(localResult.results, localResult.results.length, format);
856
- return;
857
- }
858
- const result = await runLiveAnalysis(ctx.client, siteUrl, params).catch(gscErrorHandler);
911
+ const result = await runAnalysis(buildParams(tool, args));
859
912
  if (format === "json") {
860
913
  console.log(JSON.stringify(result, null, 2));
861
914
  return;
@@ -1036,41 +1089,319 @@ const analyzeCommand = defineCommand({
1036
1089
  name: "analyze",
1037
1090
  description: "SEO analysis tools"
1038
1091
  },
1039
- subCommands: Object.fromEntries(ANALYSIS_TOOLS.map((tool) => [tool, makeToolCommand(tool)]))
1092
+ subCommands: {
1093
+ list: defineCommand({
1094
+ meta: {
1095
+ name: "list",
1096
+ description: "List available analyzer ids"
1097
+ },
1098
+ args: { json: {
1099
+ type: "boolean",
1100
+ default: false,
1101
+ description: "Output as JSON"
1102
+ } },
1103
+ async run({ args }) {
1104
+ if (args.json) {
1105
+ console.log(JSON.stringify(ANALYSIS_TOOLS, null, 2));
1106
+ return;
1107
+ }
1108
+ for (const id of ANALYSIS_TOOLS) console.log(id);
1109
+ }
1110
+ }),
1111
+ ...Object.fromEntries(ANALYSIS_TOOLS.map((tool) => [tool, makeToolCommand(tool)]))
1112
+ }
1113
+ });
1114
+ const ROOT_DIR = path.join(os.homedir(), ".config", "gscdump");
1115
+ const PROFILES_DIR = path.join(ROOT_DIR, "profiles");
1116
+ const ACTIVE_MARKER = path.join(ROOT_DIR, "active-profile");
1117
+ let activeOverride = null;
1118
+ let configDirOverridden = false;
1119
+ function getProfileDir(name) {
1120
+ return path.join(PROFILES_DIR, name);
1121
+ }
1122
+ function readActiveMarkerSync() {
1123
+ if (!fs$1.existsSync(ACTIVE_MARKER)) return null;
1124
+ return fs$1.readFileSync(ACTIVE_MARKER, "utf-8").trim() || null;
1125
+ }
1126
+ function resolveActiveProfile() {
1127
+ return activeOverride ?? process.env.GSCDUMP_PROFILE ?? readActiveMarkerSync();
1128
+ }
1129
+ function applyProfileFromCli(opts) {
1130
+ if (opts.configDir) {
1131
+ setConfigDir(opts.configDir);
1132
+ configDirOverridden = true;
1133
+ return;
1134
+ }
1135
+ if (opts.profile) {
1136
+ activeOverride = opts.profile;
1137
+ setConfigDir(getProfileDir(opts.profile));
1138
+ return;
1139
+ }
1140
+ const envProfile = process.env.GSCDUMP_PROFILE;
1141
+ if (envProfile) {
1142
+ setConfigDir(getProfileDir(envProfile));
1143
+ return;
1144
+ }
1145
+ const marker = readActiveMarkerSync();
1146
+ if (marker) setConfigDir(getProfileDir(marker));
1147
+ }
1148
+ async function setActiveProfile(name) {
1149
+ await fs.mkdir(ROOT_DIR, {
1150
+ recursive: true,
1151
+ mode: 448
1152
+ });
1153
+ if (name == null) {
1154
+ await fs.rm(ACTIVE_MARKER, { force: true }).catch(() => {});
1155
+ return;
1156
+ }
1157
+ await fs.writeFile(ACTIVE_MARKER, name, { mode: 384 });
1158
+ }
1159
+ async function listProfiles() {
1160
+ return fs.readdir(PROFILES_DIR).then((entries) => entries.filter((e) => !e.startsWith(".")).sort()).catch(() => []);
1161
+ }
1162
+ async function createProfile(name) {
1163
+ const dir = getProfileDir(name);
1164
+ await fs.mkdir(dir, {
1165
+ recursive: true,
1166
+ mode: 448
1167
+ });
1168
+ return dir;
1169
+ }
1170
+ function profileNameFromEmail(email) {
1171
+ return email.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1172
+ }
1173
+ async function adoptCurrentConfigAsProfile(name) {
1174
+ if (configDirOverridden) return null;
1175
+ if (resolveActiveProfile()) return null;
1176
+ const currentDir = getConfigDir();
1177
+ const targetDir = getProfileDir(name);
1178
+ if (currentDir === targetDir) return targetDir;
1179
+ await fs.mkdir(targetDir, {
1180
+ recursive: true,
1181
+ mode: 448
1182
+ });
1183
+ for (const f of ["tokens.json", "config.json"]) {
1184
+ const src = path.join(currentDir, f);
1185
+ const dst = path.join(targetDir, f);
1186
+ if (await fs.stat(src).then(() => true).catch(() => false)) await fs.rename(src, dst).catch(() => {});
1187
+ }
1188
+ await setActiveProfile(name);
1189
+ activeOverride = name;
1190
+ setConfigDir(targetDir);
1191
+ return targetDir;
1192
+ }
1193
+ const profileCommand = defineCommand({
1194
+ meta: {
1195
+ name: "profile",
1196
+ description: "Manage gscdump profiles (per-account token + config dirs)"
1197
+ },
1198
+ subCommands: {
1199
+ list: defineCommand({
1200
+ meta: {
1201
+ name: "list",
1202
+ description: "List configured profiles"
1203
+ },
1204
+ args: { ...OUTPUT_ARGS },
1205
+ async run({ args }) {
1206
+ const { json } = applyOutputMode(args);
1207
+ const names = await listProfiles();
1208
+ const active = resolveActiveProfile();
1209
+ if (json) {
1210
+ console.log(JSON.stringify({
1211
+ active,
1212
+ profiles: names
1213
+ }, null, 2));
1214
+ return;
1215
+ }
1216
+ if (names.length === 0) {
1217
+ logger.warn(`No profiles in ${PROFILES_DIR}`);
1218
+ logger.info("Run `gscdump auth login` to create one automatically, or `gscdump profile create <name>`");
1219
+ return;
1220
+ }
1221
+ for (const n of names) console.log(`${n === active ? "*" : " "} ${n}`);
1222
+ }
1223
+ }),
1224
+ path: defineCommand({
1225
+ meta: {
1226
+ name: "path",
1227
+ description: "Print the config directory for a profile"
1228
+ },
1229
+ args: { name: {
1230
+ type: "positional",
1231
+ required: false,
1232
+ description: "Profile name (default: active)"
1233
+ } },
1234
+ async run({ args }) {
1235
+ const name = args.name ? String(args.name) : resolveActiveProfile();
1236
+ if (!name) {
1237
+ logger.error("No profile specified and none active (set --profile, GSCDUMP_PROFILE, or run `gscdump profile use <name>`)");
1238
+ process.exit(1);
1239
+ }
1240
+ console.log(getProfileDir(name));
1241
+ }
1242
+ }),
1243
+ current: defineCommand({
1244
+ meta: {
1245
+ name: "current",
1246
+ description: "Print the active profile name"
1247
+ },
1248
+ async run() {
1249
+ const active = resolveActiveProfile();
1250
+ if (!active) process.exit(1);
1251
+ console.log(active);
1252
+ }
1253
+ }),
1254
+ use: defineCommand({
1255
+ meta: {
1256
+ name: "use",
1257
+ description: "Set the persisted active profile (subsequent commands no longer need --profile)"
1258
+ },
1259
+ args: {
1260
+ ...OUTPUT_ARGS,
1261
+ name: {
1262
+ type: "positional",
1263
+ required: true,
1264
+ description: "Profile name"
1265
+ }
1266
+ },
1267
+ async run({ args }) {
1268
+ applyOutputMode(args);
1269
+ const name = String(args.name);
1270
+ const dir = getProfileDir(name);
1271
+ if (!await fs.stat(dir).then(() => true).catch(() => false)) {
1272
+ logger.error(`Profile not found: ${name}`);
1273
+ logger.info(`Create it with: gscdump profile create ${name}`);
1274
+ process.exit(1);
1275
+ }
1276
+ await setActiveProfile(name);
1277
+ logger.success(`Active profile: ${name}`);
1278
+ }
1279
+ }),
1280
+ create: defineCommand({
1281
+ meta: {
1282
+ name: "create",
1283
+ description: "Create an empty profile directory"
1284
+ },
1285
+ args: {
1286
+ ...OUTPUT_ARGS,
1287
+ "name": {
1288
+ type: "positional",
1289
+ required: true,
1290
+ description: "Profile name"
1291
+ },
1292
+ "no-use": {
1293
+ type: "boolean",
1294
+ default: false,
1295
+ description: "Do not mark the new profile as active"
1296
+ }
1297
+ },
1298
+ async run({ args }) {
1299
+ applyOutputMode(args);
1300
+ const name = String(args.name);
1301
+ const dir = await createProfile(name);
1302
+ if (!args["no-use"]) await setActiveProfile(name);
1303
+ logger.success(`Created profile: ${name}${args["no-use"] ? "" : " (active)"}`);
1304
+ logger.info(displayPath(dir));
1305
+ }
1306
+ }),
1307
+ clear: defineCommand({
1308
+ meta: {
1309
+ name: "clear",
1310
+ description: "Clear the persisted active profile (commands fall back to root config dir)"
1311
+ },
1312
+ args: { ...OUTPUT_ARGS },
1313
+ async run({ args }) {
1314
+ applyOutputMode(args);
1315
+ await setActiveProfile(null);
1316
+ logger.success("Cleared active profile");
1317
+ }
1318
+ }),
1319
+ delete: defineCommand({
1320
+ meta: {
1321
+ name: "delete",
1322
+ description: "Remove a profile directory (tokens + config)"
1323
+ },
1324
+ args: {
1325
+ ...OUTPUT_ARGS,
1326
+ name: {
1327
+ type: "positional",
1328
+ required: true,
1329
+ description: "Profile name"
1330
+ },
1331
+ yes: {
1332
+ type: "boolean",
1333
+ alias: "y",
1334
+ default: false,
1335
+ description: "Skip confirmation"
1336
+ }
1337
+ },
1338
+ async run({ args }) {
1339
+ applyOutputMode(args);
1340
+ const name = String(args.name);
1341
+ const dir = getProfileDir(name);
1342
+ if (!await fs.stat(dir).then(() => true).catch(() => false)) {
1343
+ logger.error(`Profile not found: ${name}`);
1344
+ process.exit(1);
1345
+ }
1346
+ if (!args.yes) {
1347
+ const ok = await confirm({
1348
+ message: `Delete profile "${name}" at ${dir}? Tokens and config will be lost.`,
1349
+ initialValue: false
1350
+ });
1351
+ if (isCancel(ok) || !ok) {
1352
+ logger.info("Cancelled");
1353
+ process.exit(0);
1354
+ }
1355
+ }
1356
+ await fs.rm(dir, {
1357
+ recursive: true,
1358
+ force: true
1359
+ });
1360
+ if (readActiveMarkerSync() === name) await setActiveProfile(null);
1361
+ logger.success(`Removed profile: ${name}`);
1362
+ }
1363
+ })
1364
+ }
1040
1365
  });
1366
+ const REQUIRED_SCOPES$1 = [
1367
+ "https://www.googleapis.com/auth/webmasters",
1368
+ "https://www.googleapis.com/auth/indexing",
1369
+ "https://www.googleapis.com/auth/siteverification"
1370
+ ];
1041
1371
  async function fetchTokenInfo(accessToken) {
1042
1372
  return ofetch("https://oauth2.googleapis.com/tokeninfo", { query: { access_token: accessToken } }).catch(() => null);
1043
1373
  }
1374
+ async function resolveLiveAuthState() {
1375
+ const tokens = await loadTokens();
1376
+ const byok = resolveBYOK();
1377
+ let liveToken = null;
1378
+ if (typeof byok === "string") liveToken = byok;
1379
+ else if (byok && "getAccessToken" in byok) liveToken = await byok.getAccessToken().then((r) => r.token ?? null).catch(() => null);
1380
+ else if (tokens?.access_token) liveToken = tokens.access_token;
1381
+ const tokenInfo = liveToken ? await fetchTokenInfo(liveToken) : null;
1382
+ const scopes = tokenInfo?.scope ? tokenInfo.scope.split(/\s+/).filter(Boolean) : [];
1383
+ const has = (s) => scopes.includes(s) || scopes.includes(s.replace(".readonly", ""));
1384
+ const missing = REQUIRED_SCOPES$1.filter((s) => !has(s));
1385
+ return {
1386
+ byok,
1387
+ tokens,
1388
+ liveToken,
1389
+ tokenInfo,
1390
+ scopes,
1391
+ missing
1392
+ };
1393
+ }
1044
1394
  const statusCommand$1 = defineCommand({
1045
1395
  meta: {
1046
1396
  name: "status",
1047
1397
  description: "Show current authentication status"
1048
1398
  },
1049
- args: {
1050
- json: {
1051
- type: "boolean",
1052
- default: false,
1053
- description: "Output as JSON"
1054
- },
1055
- quiet: {
1056
- type: "boolean",
1057
- alias: "q",
1058
- default: false,
1059
- description: "Suppress info/success output"
1060
- }
1061
- },
1399
+ args: { ...OUTPUT_ARGS },
1062
1400
  async run({ args }) {
1063
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
1064
- const tokens = await loadTokens();
1065
- const byok = resolveBYOK();
1401
+ const { json } = applyOutputMode(args);
1402
+ const { byok, tokens, tokenInfo, scopes, missing } = await resolveLiveAuthState();
1066
1403
  const byokKind = byok ? typeof byok === "string" ? "access-token" : "refresh-token" : null;
1067
- let liveToken = null;
1068
- if (typeof byok === "string") liveToken = byok;
1069
- else if (byok && "getAccessToken" in byok) liveToken = await byok.getAccessToken().then((r) => r.token ?? null).catch(() => null);
1070
- else if (tokens?.access_token) liveToken = tokens.access_token;
1071
- const tokenInfo = liveToken ? await fetchTokenInfo(liveToken) : null;
1072
- const scopes = tokenInfo?.scope ? tokenInfo.scope.split(/\s+/).filter(Boolean) : [];
1073
- if (args.json) {
1404
+ if (json) {
1074
1405
  console.log(JSON.stringify({
1075
1406
  authenticated: !!tokens || !!byok,
1076
1407
  source: byok ? "byok" : tokens ? "saved-tokens" : null,
@@ -1089,14 +1420,7 @@ const statusCommand$1 = defineCommand({
1089
1420
  const reportScopes = () => {
1090
1421
  if (scopes.length === 0) return;
1091
1422
  console.log(` Scopes:`);
1092
- const required = [
1093
- "https://www.googleapis.com/auth/webmasters",
1094
- "https://www.googleapis.com/auth/indexing",
1095
- "https://www.googleapis.com/auth/siteverification"
1096
- ];
1097
- const has = (s) => scopes.includes(s) || scopes.includes(s.replace(".readonly", ""));
1098
1423
  for (const s of scopes) console.log(` \x1B[90m└─\x1B[0m ${s}`);
1099
- const missing = required.filter((s) => !has(s));
1100
1424
  if (missing.length > 0) {
1101
1425
  console.log(` \x1B[33mMissing scopes:\x1B[0m`);
1102
1426
  for (const s of missing) console.log(` \x1B[90m└─\x1B[0m ${s}`);
@@ -1136,14 +1460,9 @@ const refreshCommand = defineCommand({
1136
1460
  name: "refresh",
1137
1461
  description: "Force-refresh saved OAuth tokens (no-op for BYOK)"
1138
1462
  },
1139
- args: { quiet: {
1140
- type: "boolean",
1141
- alias: "q",
1142
- default: false,
1143
- description: "Suppress info/success output"
1144
- } },
1463
+ args: { ...OUTPUT_ARGS },
1145
1464
  async run({ args }) {
1146
- setQuiet(Boolean(args.quiet));
1465
+ applyOutputMode(args);
1147
1466
  if (resolveBYOK()) {
1148
1467
  logger.info("BYOK detected; refresh handled per-call by the SDK");
1149
1468
  return;
@@ -1181,6 +1500,7 @@ const authCommand = defineCommand({
1181
1500
  description: "Run OAuth flow and persist tokens (skip if BYOK env vars set)"
1182
1501
  },
1183
1502
  args: {
1503
+ ...OUTPUT_ARGS,
1184
1504
  "force": {
1185
1505
  type: "boolean",
1186
1506
  alias: "f",
@@ -1195,22 +1515,17 @@ const authCommand = defineCommand({
1195
1515
  "service-account": {
1196
1516
  type: "string",
1197
1517
  description: "Path to a service-account JSON key (skips OAuth)"
1198
- },
1199
- "quiet": {
1200
- type: "boolean",
1201
- alias: "q",
1202
- default: false,
1203
- description: "Suppress info/success output"
1204
1518
  }
1205
1519
  },
1206
1520
  async run({ args }) {
1207
- setQuiet(Boolean(args.quiet));
1521
+ applyOutputMode(args);
1208
1522
  if (resolveBYOK() && !args.force) {
1209
1523
  logger.info("BYOK env vars detected, no login needed (--force to override)");
1210
1524
  return;
1211
1525
  }
1212
1526
  if (args["service-account"]) {
1213
- const jwt = await loadServiceAccount(String(args["service-account"])).catch((e) => {
1527
+ const saPath = path.resolve(String(args["service-account"]));
1528
+ const jwt = await loadServiceAccount(saPath).catch((e) => {
1214
1529
  logger.error(`Service-account load failed: ${e.message}`);
1215
1530
  process.exit(1);
1216
1531
  });
@@ -1218,8 +1533,11 @@ const authCommand = defineCommand({
1218
1533
  logger.error(`Service-account auth failed: ${e.message}`);
1219
1534
  process.exit(1);
1220
1535
  });
1536
+ const config = await loadConfig();
1537
+ config.serviceAccountPath = saPath;
1538
+ await saveConfig(config);
1221
1539
  logger.success(`Service-account verified: ${jwt.email ?? "OK"}`);
1222
- logger.info(`Set GOOGLE_APPLICATION_CREDENTIALS=${args["service-account"]} to use it across sessions.`);
1540
+ logger.info(`Saved path to config: ${saPath}`);
1223
1541
  return;
1224
1542
  }
1225
1543
  if (args.force) await clearTokens();
@@ -1232,6 +1550,14 @@ const authCommand = defineCommand({
1232
1550
  process.exit(1);
1233
1551
  });
1234
1552
  logger.success("Logged in");
1553
+ if (!resolveActiveProfile()) {
1554
+ const tokens = await loadTokens();
1555
+ const info = tokens?.access_token ? await fetchTokenInfo(tokens.access_token) : null;
1556
+ if (info?.email) {
1557
+ const name = profileNameFromEmail(info.email);
1558
+ if (await adoptCurrentConfigAsProfile(name).catch(() => null)) logger.success(`Saved as profile "${name}" (active)`);
1559
+ }
1560
+ }
1235
1561
  if (resolveBYOK()) {
1236
1562
  console.log();
1237
1563
  console.log(await formatAuthProvenance());
@@ -1243,18 +1569,44 @@ const authCommand = defineCommand({
1243
1569
  name: "logout",
1244
1570
  description: "Clear stored OAuth tokens"
1245
1571
  },
1246
- args: { quiet: {
1247
- type: "boolean",
1248
- alias: "q",
1249
- default: false,
1250
- description: "Suppress info/success output"
1251
- } },
1572
+ args: { ...OUTPUT_ARGS },
1252
1573
  async run({ args }) {
1253
- setQuiet(Boolean(args.quiet));
1574
+ applyOutputMode(args);
1254
1575
  await clearTokens();
1576
+ const config = await loadConfig();
1577
+ if (config.serviceAccountPath) {
1578
+ delete config.serviceAccountPath;
1579
+ await saveConfig(config);
1580
+ logger.info("Cleared saved service-account path");
1581
+ }
1255
1582
  }
1256
1583
  }),
1257
- refresh: refreshCommand
1584
+ refresh: refreshCommand,
1585
+ scopes: defineCommand({
1586
+ meta: {
1587
+ name: "scopes",
1588
+ description: "Print granted OAuth scopes (one per line); exits 1 if any required scope is missing"
1589
+ },
1590
+ args: { ...OUTPUT_ARGS },
1591
+ async run({ args }) {
1592
+ const { json } = applyOutputMode(args);
1593
+ const { liveToken, scopes, missing } = await resolveLiveAuthState();
1594
+ if (!liveToken) {
1595
+ if (json) console.log(JSON.stringify({
1596
+ scopes: [],
1597
+ missing: null
1598
+ }, null, 2));
1599
+ else logger.error("Not authenticated");
1600
+ process.exit(1);
1601
+ }
1602
+ if (json) console.log(JSON.stringify({
1603
+ scopes,
1604
+ missing
1605
+ }, null, 2));
1606
+ else for (const s of scopes) console.log(s);
1607
+ if (missing.length > 0) process.exit(1);
1608
+ }
1609
+ })
1258
1610
  }
1259
1611
  });
1260
1612
  const showCommand = defineCommand({
@@ -1262,24 +1614,12 @@ const showCommand = defineCommand({
1262
1614
  name: "show",
1263
1615
  description: "Show current config"
1264
1616
  },
1265
- args: {
1266
- json: {
1267
- type: "boolean",
1268
- default: false,
1269
- description: "Output config as a single JSON object (suppresses path header)"
1270
- },
1271
- quiet: {
1272
- type: "boolean",
1273
- alias: "q",
1274
- default: false,
1275
- description: "Suppress info/success output"
1276
- }
1277
- },
1617
+ args: { ...OUTPUT_ARGS },
1278
1618
  async run({ args }) {
1279
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
1619
+ const { json } = applyOutputMode(args);
1280
1620
  const config = await loadConfig();
1281
1621
  const configPath = getConfigPath();
1282
- if (args.json) {
1622
+ if (json) {
1283
1623
  console.log(JSON.stringify({
1284
1624
  path: configPath,
1285
1625
  config
@@ -1303,7 +1643,8 @@ const VALID_KEYS = [
1303
1643
  "dataDir",
1304
1644
  "defaultLimit",
1305
1645
  "defaultSearchType",
1306
- "defaultDataState"
1646
+ "defaultDataState",
1647
+ "serviceAccountPath"
1307
1648
  ];
1308
1649
  const NUMERIC_KEYS = new Set(["defaultLimit"]);
1309
1650
  const configCommand = defineCommand({
@@ -1329,15 +1670,10 @@ const configCommand = defineCommand({
1329
1670
  description: "Value to set",
1330
1671
  required: true
1331
1672
  },
1332
- quiet: {
1333
- type: "boolean",
1334
- alias: "q",
1335
- default: false,
1336
- description: "Suppress info/success output"
1337
- }
1673
+ ...OUTPUT_ARGS
1338
1674
  },
1339
1675
  async run({ args }) {
1340
- setQuiet(Boolean(args.quiet));
1676
+ applyOutputMode(args);
1341
1677
  if (!VALID_KEYS.includes(args.key)) {
1342
1678
  logger.error(`Invalid key: ${args.key}`);
1343
1679
  logger.info(`Valid keys: ${VALID_KEYS.join(", ")}`);
@@ -1365,15 +1701,10 @@ const configCommand = defineCommand({
1365
1701
  description: `Config key to remove (${VALID_KEYS.join(", ")})`,
1366
1702
  required: true
1367
1703
  },
1368
- quiet: {
1369
- type: "boolean",
1370
- alias: "q",
1371
- default: false,
1372
- description: "Suppress info/success output"
1373
- }
1704
+ ...OUTPUT_ARGS
1374
1705
  },
1375
1706
  async run({ args }) {
1376
- setQuiet(Boolean(args.quiet));
1707
+ applyOutputMode(args);
1377
1708
  if (!VALID_KEYS.includes(args.key)) {
1378
1709
  logger.error(`Invalid key: ${args.key}`);
1379
1710
  logger.info(`Valid keys: ${VALID_KEYS.join(", ")}`);
@@ -1399,21 +1730,9 @@ const configCommand = defineCommand({
1399
1730
  name: "validate",
1400
1731
  description: "Validate the saved config (defaultSite is verified, dataDir exists/writable)"
1401
1732
  },
1402
- args: {
1403
- json: {
1404
- type: "boolean",
1405
- default: false,
1406
- description: "Output as JSON"
1407
- },
1408
- quiet: {
1409
- type: "boolean",
1410
- alias: "q",
1411
- default: false,
1412
- description: "Suppress info/success output"
1413
- }
1414
- },
1733
+ args: { ...OUTPUT_ARGS },
1415
1734
  async run({ args }) {
1416
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
1735
+ const { json } = applyOutputMode(args);
1417
1736
  const { resolveDataDir } = await import("./_chunks/config.mjs").then((n) => n.t);
1418
1737
  const fs = await import("node:fs/promises");
1419
1738
  const config = await loadConfig();
@@ -1459,7 +1778,31 @@ const configCommand = defineCommand({
1459
1778
  level: "fail",
1460
1779
  message: `unknown format: ${config.defaultFormat}`
1461
1780
  });
1462
- if (args.json) {
1781
+ const { SearchTypes } = await import("gscdump/query");
1782
+ const allowedSearchTypes = Object.values(SearchTypes);
1783
+ if (config.defaultSearchType && !allowedSearchTypes.includes(config.defaultSearchType)) issues.push({
1784
+ key: "defaultSearchType",
1785
+ level: "fail",
1786
+ message: `unknown search type: ${config.defaultSearchType} (allowed: ${allowedSearchTypes.join(", ")})`
1787
+ });
1788
+ const allowedDataStates = [
1789
+ "all",
1790
+ "final",
1791
+ "hourly_all"
1792
+ ];
1793
+ if (config.defaultDataState && !allowedDataStates.includes(config.defaultDataState)) issues.push({
1794
+ key: "defaultDataState",
1795
+ level: "fail",
1796
+ message: `unknown data state: ${config.defaultDataState} (allowed: ${allowedDataStates.join(", ")})`
1797
+ });
1798
+ if (config.serviceAccountPath) {
1799
+ if (!await fs.stat(config.serviceAccountPath).catch(() => null)) issues.push({
1800
+ key: "serviceAccountPath",
1801
+ level: "fail",
1802
+ message: `${displayPath(config.serviceAccountPath)} does not exist`
1803
+ });
1804
+ }
1805
+ if (json) {
1463
1806
  console.log(JSON.stringify({
1464
1807
  ok: !issues.some((i) => i.level === "fail"),
1465
1808
  issues
@@ -1798,12 +2141,9 @@ const doctorCommand = defineCommand({
1798
2141
  name: "doctor",
1799
2142
  description: "Run health checks (env, auth, scopes, time, dataDir, store, GSC reachability + ping, defaultSite)"
1800
2143
  },
1801
- args: { json: {
1802
- type: "boolean",
1803
- default: false,
1804
- description: "Output as JSON"
1805
- } },
2144
+ args: { ...OUTPUT_ARGS },
1806
2145
  async run({ args }) {
2146
+ const { json } = applyOutputMode(args);
1807
2147
  const envResult = await checkEnv();
1808
2148
  const [authResult, timeChecks, dataDirChecks, watermarkChecks, gscApi, indexingApi, siteVerificationApi] = await Promise.all([
1809
2149
  checkAuth$1(envResult.envKeys),
@@ -1830,7 +2170,7 @@ const doctorCommand = defineCommand({
1830
2170
  ...siteVerificationApi,
1831
2171
  ...sitesChecks
1832
2172
  ];
1833
- if (args.json) {
2173
+ if (json) {
1834
2174
  console.log(JSON.stringify({
1835
2175
  checks: all,
1836
2176
  ok: all.every((c) => c.status !== "fail")
@@ -1904,20 +2244,10 @@ const dumpCommand = defineCommand({
1904
2244
  default: false,
1905
2245
  description: "Compact every closed month into a single file before exporting"
1906
2246
  },
1907
- "json": {
1908
- type: "boolean",
1909
- default: false,
1910
- description: "Emit a JSON summary of what was exported"
1911
- },
1912
- "quiet": {
1913
- type: "boolean",
1914
- alias: "q",
1915
- default: false,
1916
- description: "Suppress progress output"
1917
- }
2247
+ ...OUTPUT_ARGS
1918
2248
  },
1919
2249
  async run({ args }) {
1920
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
2250
+ const { json, quiet } = applyOutputMode(args);
1921
2251
  const format = String(args.format);
1922
2252
  if (!FORMATS.includes(format)) {
1923
2253
  logger.error(`Invalid --format: ${format}. Allowed: ${FORMATS.join(", ")}`);
@@ -1935,12 +2265,12 @@ const dumpCommand = defineCommand({
1935
2265
  logger.warn("No sites with local data. Run `gscdump sync` first.");
1936
2266
  process.exit(0);
1937
2267
  }
1938
- if (args.compact) for (const siteUrl of targets) await compactClosedMonths(store, siteUrl, args.quiet);
2268
+ if (args.compact) for (const siteUrl of targets) await compactClosedMonths(store, siteUrl, quiet);
1939
2269
  const summary = [];
1940
2270
  for (const siteUrl of targets) {
1941
2271
  const entries = (await listLiveEntries(store, siteUrl)).filter((e) => !tablesFilter || tablesFilter.has(e.table));
1942
2272
  if (entries.length === 0) {
1943
- if (!args.json && !args.quiet) logger.warn(`No data for ${siteUrl}; skipping`);
2273
+ if (!quiet) logger.warn(`No data for ${siteUrl}; skipping`);
1944
2274
  continue;
1945
2275
  }
1946
2276
  if (format === "parquet") {
@@ -1963,7 +2293,7 @@ const dumpCommand = defineCommand({
1963
2293
  });
1964
2294
  }
1965
2295
  }
1966
- if (args.json) {
2296
+ if (json) {
1967
2297
  console.log(JSON.stringify({
1968
2298
  outDir,
1969
2299
  sites: summary
@@ -2092,20 +2422,10 @@ const inspectSubCommand = defineCommand({
2092
2422
  default: "4",
2093
2423
  description: "Concurrent in-flight inspect calls (default: 4)"
2094
2424
  },
2095
- json: {
2096
- type: "boolean",
2097
- default: false,
2098
- description: "Emit a JSON summary of inspection results"
2099
- },
2100
- quiet: {
2101
- type: "boolean",
2102
- alias: "q",
2103
- default: false,
2104
- description: "Suppress progress output"
2105
- }
2425
+ ...OUTPUT_ARGS
2106
2426
  },
2107
2427
  async run({ args }) {
2108
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
2428
+ const { json, quiet } = applyOutputMode(args);
2109
2429
  const ctx = await createCommandContext({
2110
2430
  needsAuth: true,
2111
2431
  needsStore: true
@@ -2115,7 +2435,6 @@ const inspectSubCommand = defineCommand({
2115
2435
  const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
2116
2436
  const limit = args.limit ? Number.parseInt(String(args.limit), 10) : INSPECTION_QPD_PER_PROPERTY;
2117
2437
  const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 4);
2118
- const quiet = Boolean(args.quiet) || Boolean(args.json);
2119
2438
  const urls = (await readUrlList({ file: args.file ? String(args.file) : void 0 })).slice(0, limit);
2120
2439
  if (urls.length === 0) {
2121
2440
  logger.warn("No URLs to inspect.");
@@ -2163,7 +2482,7 @@ const inspectSubCommand = defineCommand({
2163
2482
  userId: store.userId,
2164
2483
  siteId: store.siteIdFor(siteUrl)
2165
2484
  }, records);
2166
- if (args.json) console.log(JSON.stringify({
2485
+ if (json) console.log(JSON.stringify({
2167
2486
  site: siteUrl,
2168
2487
  inspected: records.length,
2169
2488
  failed,
@@ -2187,6 +2506,7 @@ const showSubCommand = defineCommand({
2187
2506
  description: "Print the latest inspection record for a URL from the local entity store"
2188
2507
  },
2189
2508
  args: {
2509
+ ...OUTPUT_ARGS,
2190
2510
  site: {
2191
2511
  type: "string",
2192
2512
  alias: "s",
@@ -2196,14 +2516,10 @@ const showSubCommand = defineCommand({
2196
2516
  type: "positional",
2197
2517
  required: true,
2198
2518
  description: "URL to look up"
2199
- },
2200
- json: {
2201
- type: "boolean",
2202
- default: false,
2203
- description: "Output as JSON"
2204
2519
  }
2205
2520
  },
2206
2521
  async run({ args }) {
2522
+ const { json } = applyOutputMode(args);
2207
2523
  const ctx = await createCommandContext({
2208
2524
  needsAuth: true,
2209
2525
  needsStore: true
@@ -2218,7 +2534,7 @@ const showSubCommand = defineCommand({
2218
2534
  logger.warn(`No inspection record for ${args.url}`);
2219
2535
  process.exit(1);
2220
2536
  }
2221
- if (args.json) {
2537
+ if (json) {
2222
2538
  console.log(JSON.stringify(record, null, 2));
2223
2539
  return;
2224
2540
  }
@@ -2240,24 +2556,15 @@ const sitemapsSnapshotSubCommand = defineCommand({
2240
2556
  description: "Fetch current sitemap state from GSC and persist to the local entity store"
2241
2557
  },
2242
2558
  args: {
2559
+ ...OUTPUT_ARGS,
2243
2560
  site: {
2244
2561
  type: "string",
2245
2562
  alias: "s",
2246
2563
  description: "Site URL (e.g., sc-domain:example.com); defaults to config.defaultSite or prompt"
2247
- },
2248
- quiet: {
2249
- type: "boolean",
2250
- alias: "q",
2251
- default: false,
2252
- description: "Suppress progress output"
2253
- },
2254
- json: {
2255
- type: "boolean",
2256
- default: false,
2257
- description: "Emit the snapshot JSON to stdout"
2258
2564
  }
2259
2565
  },
2260
2566
  async run({ args }) {
2567
+ const { json, quiet } = applyOutputMode(args);
2261
2568
  const ctx = await createCommandContext({
2262
2569
  needsAuth: true,
2263
2570
  needsStore: true
@@ -2265,7 +2572,6 @@ const sitemapsSnapshotSubCommand = defineCommand({
2265
2572
  const client = ctx.client;
2266
2573
  const store = ctx.store;
2267
2574
  const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
2268
- const quiet = Boolean(args.quiet);
2269
2575
  const apiSitemaps = await client.sitemaps.list(siteUrl);
2270
2576
  const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
2271
2577
  const records = apiSitemaps.filter((s) => typeof s.path === "string").map((s) => ({
@@ -2289,7 +2595,7 @@ const sitemapsSnapshotSubCommand = defineCommand({
2289
2595
  userId: store.userId,
2290
2596
  siteId: store.siteIdFor(siteUrl)
2291
2597
  }, records);
2292
- if (args.json) {
2598
+ if (json) {
2293
2599
  console.log(JSON.stringify({
2294
2600
  site: siteUrl,
2295
2601
  capturedAt,
@@ -2314,6 +2620,7 @@ const sitemapsShowSubCommand = defineCommand({
2314
2620
  description: "Print the latest captured sitemap state for a feedpath"
2315
2621
  },
2316
2622
  args: {
2623
+ ...OUTPUT_ARGS,
2317
2624
  site: {
2318
2625
  type: "string",
2319
2626
  alias: "s",
@@ -2323,14 +2630,10 @@ const sitemapsShowSubCommand = defineCommand({
2323
2630
  type: "positional",
2324
2631
  required: true,
2325
2632
  description: "Sitemap path (feedpath)"
2326
- },
2327
- json: {
2328
- type: "boolean",
2329
- default: false,
2330
- description: "Output as JSON"
2331
2633
  }
2332
2634
  },
2333
2635
  async run({ args }) {
2636
+ const { json } = applyOutputMode(args);
2334
2637
  const ctx = await createCommandContext({
2335
2638
  needsAuth: true,
2336
2639
  needsStore: true
@@ -2345,7 +2648,7 @@ const sitemapsShowSubCommand = defineCommand({
2345
2648
  logger.warn(`No sitemap record for ${args.path}`);
2346
2649
  process.exit(1);
2347
2650
  }
2348
- if (args.json) {
2651
+ if (json) {
2349
2652
  console.log(JSON.stringify(record, null, 2));
2350
2653
  return;
2351
2654
  }
@@ -2398,14 +2701,10 @@ const indexingSubCommand = defineCommand({
2398
2701
  default: "4",
2399
2702
  description: "Concurrent in-flight getMetadata calls (default: 4)"
2400
2703
  },
2401
- quiet: {
2402
- type: "boolean",
2403
- alias: "q",
2404
- default: false,
2405
- description: "Suppress progress output"
2406
- }
2704
+ ...OUTPUT_ARGS
2407
2705
  },
2408
2706
  async run({ args }) {
2707
+ const { quiet } = applyOutputMode(args);
2409
2708
  const ctx = await createCommandContext({
2410
2709
  needsAuth: true,
2411
2710
  needsStore: true
@@ -2414,7 +2713,6 @@ const indexingSubCommand = defineCommand({
2414
2713
  const store = ctx.store;
2415
2714
  const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
2416
2715
  const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 4);
2417
- const quiet = Boolean(args.quiet);
2418
2716
  const urls = await readUrlList({ file: args.file ? String(args.file) : void 0 });
2419
2717
  if (urls.length === 0) {
2420
2718
  logger.warn("No URLs to fetch metadata for.");
@@ -2490,6 +2788,14 @@ function parseRetries(v) {
2490
2788
  const n = Number.parseInt(String(v), 10);
2491
2789
  return Number.isFinite(n) && n >= 0 ? n : void 0;
2492
2790
  }
2791
+ async function resolveUrlSource(args) {
2792
+ const fromSitemap = args["from-sitemap"];
2793
+ if (fromSitemap) return fetchSitemapUrls(String(fromSitemap)).catch((e) => {
2794
+ logger.error(`Sitemap fetch failed: ${e.message}`);
2795
+ process.exit(1);
2796
+ });
2797
+ return readUrlList$1(args);
2798
+ }
2493
2799
  const submitCommand$1 = defineCommand({
2494
2800
  meta: {
2495
2801
  name: "submit",
@@ -2501,21 +2807,11 @@ const submitCommand$1 = defineCommand({
2501
2807
  required: true,
2502
2808
  description: "URL to submit"
2503
2809
  },
2504
- json: {
2505
- type: "boolean",
2506
- default: false,
2507
- description: "Output as JSON"
2508
- },
2509
- quiet: {
2510
- type: "boolean",
2511
- alias: "q",
2512
- default: false,
2513
- description: "Suppress info/success output"
2514
- },
2810
+ ...OUTPUT_ARGS,
2515
2811
  ...RETRIES_ARG
2516
2812
  },
2517
2813
  async run({ args }) {
2518
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
2814
+ applyOutputMode(args);
2519
2815
  const result = await requestIndexing((await createCommandContext({
2520
2816
  needsAuth: true,
2521
2817
  fetchOptions: { retry: parseRetries(args.retries) }
@@ -2539,21 +2835,11 @@ const removeCommand = defineCommand({
2539
2835
  required: true,
2540
2836
  description: "URL to mark removed"
2541
2837
  },
2542
- json: {
2543
- type: "boolean",
2544
- default: false,
2545
- description: "Output as JSON"
2546
- },
2547
- quiet: {
2548
- type: "boolean",
2549
- alias: "q",
2550
- default: false,
2551
- description: "Suppress info/success output"
2552
- },
2838
+ ...OUTPUT_ARGS,
2553
2839
  ...RETRIES_ARG
2554
2840
  },
2555
2841
  async run({ args }) {
2556
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
2842
+ applyOutputMode(args);
2557
2843
  const result = await requestIndexing((await createCommandContext({
2558
2844
  needsAuth: true,
2559
2845
  fetchOptions: { retry: parseRetries(args.retries) }
@@ -2590,7 +2876,7 @@ const statusCommand = defineCommand({
2590
2876
  }
2591
2877
  },
2592
2878
  async run({ args }) {
2593
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
2879
+ applyOutputMode(args);
2594
2880
  const meta = await getIndexingMetadata((await createCommandContext({ needsAuth: true })).client, args.url).catch(gscErrorHandler);
2595
2881
  if (args.json) {
2596
2882
  console.log(JSON.stringify(meta, null, 2));
@@ -2610,31 +2896,65 @@ const statusCommand = defineCommand({
2610
2896
  }
2611
2897
  });
2612
2898
  const INDEXING_DAILY_QUOTA = 200;
2899
+ const INDEXING_PER_MINUTE_QUOTA = 600;
2900
+ const quotaCommand = defineCommand({
2901
+ meta: {
2902
+ name: "quota",
2903
+ description: "Show documented Indexing API quotas (no live counters; quota usage is not exposed by the API)"
2904
+ },
2905
+ args: { ...OUTPUT_ARGS },
2906
+ async run({ args }) {
2907
+ const { json } = applyOutputMode(args);
2908
+ const payload = {
2909
+ perDay: INDEXING_DAILY_QUOTA,
2910
+ perMinute: INDEXING_PER_MINUTE_QUOTA,
2911
+ note: "Documented defaults. Google does not expose live counters; track yours by counting submit calls.",
2912
+ docs: "https://developers.google.com/search/apis/indexing-api/v3/quota-pricing"
2913
+ };
2914
+ if (json) {
2915
+ console.log(JSON.stringify(payload, null, 2));
2916
+ return;
2917
+ }
2918
+ console.log();
2919
+ console.log(` \x1B[1mIndexing API quota\x1B[0m`);
2920
+ console.log(` Per day: ${payload.perDay}`);
2921
+ console.log(` Per minute: ${payload.perMinute}`);
2922
+ console.log();
2923
+ console.log(` \x1B[90m${payload.note}\x1B[0m`);
2924
+ console.log(` \x1B[90mDocs: ${payload.docs}\x1B[0m`);
2925
+ console.log();
2926
+ }
2927
+ });
2613
2928
  const indexingCommand = defineCommand({
2614
2929
  meta: {
2615
2930
  name: "indexing",
2616
2931
  description: "Notify Google about URL updates/removals (Indexing API)"
2617
2932
  },
2618
2933
  subCommands: {
2619
- submit: submitCommand$1,
2620
- remove: removeCommand,
2621
- status: statusCommand,
2622
- batch: defineCommand({
2934
+ "submit": submitCommand$1,
2935
+ "remove": removeCommand,
2936
+ "status": statusCommand,
2937
+ "batch": defineCommand({
2623
2938
  meta: {
2624
2939
  name: "batch",
2625
2940
  description: "Submit many URLs from a file or stdin (one URL per line)"
2626
2941
  },
2627
2942
  args: {
2943
+ ...OUTPUT_ARGS,
2628
2944
  "urls": {
2629
2945
  type: "positional",
2630
2946
  required: false,
2631
- description: "URLs (or use --file/stdin)"
2947
+ description: "URLs (or use --file/--from-sitemap/stdin)"
2632
2948
  },
2633
2949
  "file": {
2634
2950
  type: "string",
2635
2951
  alias: "f",
2636
2952
  description: "File with URLs (one per line)"
2637
2953
  },
2954
+ "from-sitemap": {
2955
+ type: "string",
2956
+ description: "Sitemap URL (or sitemap index) to pull URLs from"
2957
+ },
2638
2958
  "type": {
2639
2959
  type: "string",
2640
2960
  default: "URL_UPDATED",
@@ -2651,17 +2971,6 @@ const indexingCommand = defineCommand({
2651
2971
  default: "1",
2652
2972
  description: "Concurrent in-flight requests"
2653
2973
  },
2654
- "quiet": {
2655
- type: "boolean",
2656
- alias: "q",
2657
- default: false,
2658
- description: "Suppress progress output"
2659
- },
2660
- "json": {
2661
- type: "boolean",
2662
- default: false,
2663
- description: "Output as JSON"
2664
- },
2665
2974
  "yes": {
2666
2975
  type: "boolean",
2667
2976
  alias: "y",
@@ -2674,10 +2983,10 @@ const indexingCommand = defineCommand({
2674
2983
  }
2675
2984
  },
2676
2985
  async run({ args }) {
2677
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
2678
- const urls = await readUrlList$1(args);
2986
+ applyOutputMode(args);
2987
+ const urls = await resolveUrlSource(args);
2679
2988
  if (urls.length === 0) {
2680
- logger.error("No URLs provided. Pass URLs as args, --file, or stdin.");
2989
+ logger.error("No URLs provided. Pass URLs as args, --file, --from-sitemap, or stdin.");
2681
2990
  process.exit(1);
2682
2991
  }
2683
2992
  const type = String(args.type);
@@ -2710,7 +3019,71 @@ const indexingCommand = defineCommand({
2710
3019
  }
2711
3020
  if (!args.quiet) logger.success(`Submitted ${results.length}/${urls.length} URLs`);
2712
3021
  }
2713
- })
3022
+ }),
3023
+ "batch-status": defineCommand({
3024
+ meta: {
3025
+ name: "batch-status",
3026
+ description: "Get indexing notification metadata for many URLs"
3027
+ },
3028
+ args: {
3029
+ ...OUTPUT_ARGS,
3030
+ "urls": {
3031
+ type: "positional",
3032
+ required: false,
3033
+ description: "URLs (or use --file/--from-sitemap/stdin)"
3034
+ },
3035
+ "file": {
3036
+ type: "string",
3037
+ alias: "f",
3038
+ description: "File with URLs (one per line)"
3039
+ },
3040
+ "from-sitemap": {
3041
+ type: "string",
3042
+ description: "Sitemap URL (or sitemap index) to pull URLs from"
3043
+ },
3044
+ "delay-ms": {
3045
+ type: "string",
3046
+ default: "100",
3047
+ description: "Delay between requests"
3048
+ },
3049
+ "concurrency": {
3050
+ type: "string",
3051
+ alias: "c",
3052
+ default: "1",
3053
+ description: "Concurrent in-flight requests"
3054
+ },
3055
+ "retries": {
3056
+ type: "string",
3057
+ description: "Override per-call retry count (default: 3)"
3058
+ }
3059
+ },
3060
+ async run({ args }) {
3061
+ applyOutputMode(args);
3062
+ const urls = await resolveUrlSource(args);
3063
+ if (urls.length === 0) {
3064
+ logger.error("No URLs provided. Pass URLs as args, --file, --from-sitemap, or stdin.");
3065
+ process.exit(1);
3066
+ }
3067
+ const ctx = await createCommandContext({
3068
+ needsAuth: true,
3069
+ fetchOptions: { retry: parseRetries(args.retries) }
3070
+ });
3071
+ const delayMs = Number.parseInt(String(args["delay-ms"]), 10);
3072
+ const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 1);
3073
+ if (!args.json && !args.quiet) logger.info(`Fetching status for ${urls.length} URLs ...`);
3074
+ const results = await runSequentialBatch(urls, (url) => getIndexingMetadata(ctx.client, url), {
3075
+ delayMs,
3076
+ concurrency,
3077
+ onProgress: args.json || args.quiet ? void 0 : (r, i, total) => logger.info(`[${i + 1}/${total}] ${r.url}`)
3078
+ }).catch(gscErrorHandler);
3079
+ if (args.json) {
3080
+ console.log(JSON.stringify(results, null, 2));
3081
+ return;
3082
+ }
3083
+ if (!args.quiet) logger.success(`Fetched ${results.length}/${urls.length} URLs`);
3084
+ }
3085
+ }),
3086
+ "quota": quotaCommand
2714
3087
  }
2715
3088
  });
2716
3089
  const ENV_LINE_RE = /^([^=]+)=(.*)$/;
@@ -2758,15 +3131,10 @@ const initCommand = defineCommand({
2758
3131
  default: false,
2759
3132
  description: "Skip dataDir prompt (auth-only setup)"
2760
3133
  },
2761
- "quiet": {
2762
- type: "boolean",
2763
- alias: "q",
2764
- default: false,
2765
- description: "Suppress info/success output"
2766
- }
3134
+ ...OUTPUT_ARGS
2767
3135
  },
2768
3136
  async run({ args }) {
2769
- setQuiet(Boolean(args.quiet));
3137
+ applyOutputMode(args);
2770
3138
  const config = await loadConfig();
2771
3139
  if (config.clientId && config.clientSecret && !args.force) {
2772
3140
  logger.info("Already configured");
@@ -2946,111 +3314,111 @@ function printInspection(url, inspection) {
2946
3314
  }
2947
3315
  console.log();
2948
3316
  }
2949
- const inspectCommand = defineCommand({
3317
+ const batchCommand = defineCommand({
2950
3318
  meta: {
2951
- name: "inspect",
2952
- description: "Inspect URL indexing status (single URL; use `inspect batch` for many)"
3319
+ name: "batch",
3320
+ description: "Inspect many URLs from a file or stdin (one URL per line)"
2953
3321
  },
2954
3322
  args: {
2955
- site: {
3323
+ ...OUTPUT_ARGS,
3324
+ "site": {
2956
3325
  type: "string",
2957
3326
  alias: "s",
2958
3327
  description: "Site URL (defaults to config.defaultSite or prompt)"
2959
3328
  },
2960
- url: {
3329
+ "urls": {
2961
3330
  type: "positional",
2962
- required: true,
2963
- description: "URL to inspect"
3331
+ required: false,
3332
+ description: "URLs (or use --file/--from-sitemap/stdin)"
2964
3333
  },
2965
- json: {
2966
- type: "boolean",
2967
- default: false,
2968
- description: "Output as JSON"
3334
+ "file": {
3335
+ type: "string",
3336
+ alias: "f",
3337
+ description: "File with URLs (one per line)"
2969
3338
  },
2970
- quiet: {
2971
- type: "boolean",
2972
- alias: "q",
2973
- default: false,
2974
- description: "Suppress info/success output"
3339
+ "from-sitemap": {
3340
+ type: "string",
3341
+ description: "Sitemap URL (or sitemap index) to pull URLs from"
3342
+ },
3343
+ "delay-ms": {
3344
+ type: "string",
3345
+ default: "200",
3346
+ description: "Delay between requests"
3347
+ },
3348
+ "concurrency": {
3349
+ type: "string",
3350
+ alias: "c",
3351
+ default: "1",
3352
+ description: "Concurrent in-flight requests"
2975
3353
  }
2976
3354
  },
2977
- subCommands: { batch: defineCommand({
2978
- meta: {
2979
- name: "batch",
2980
- description: "Inspect many URLs from a file or stdin (one URL per line)"
2981
- },
2982
- args: {
2983
- "site": {
2984
- type: "string",
2985
- alias: "s",
2986
- description: "Site URL (defaults to config.defaultSite or prompt)"
2987
- },
2988
- "urls": {
2989
- type: "positional",
2990
- required: false,
2991
- description: "URLs (or use --file/stdin)"
2992
- },
2993
- "file": {
2994
- type: "string",
2995
- alias: "f",
2996
- description: "File with URLs (one per line)"
2997
- },
2998
- "delay-ms": {
2999
- type: "string",
3000
- default: "200",
3001
- description: "Delay between requests"
3002
- },
3003
- "concurrency": {
3004
- type: "string",
3005
- alias: "c",
3006
- default: "1",
3007
- description: "Concurrent in-flight requests"
3008
- },
3009
- "quiet": {
3010
- type: "boolean",
3011
- alias: "q",
3012
- default: false,
3013
- description: "Suppress progress output"
3014
- },
3015
- "json": {
3016
- type: "boolean",
3017
- default: false,
3018
- description: "Output as JSON"
3019
- }
3355
+ async run({ args }) {
3356
+ const { json, quiet } = applyOutputMode(args);
3357
+ const urls = args["from-sitemap"] ? await fetchSitemapUrls(String(args["from-sitemap"])).catch((e) => {
3358
+ logger.error(`Sitemap fetch failed: ${e.message}`);
3359
+ process.exit(1);
3360
+ }) : await readUrlList$1(args);
3361
+ if (urls.length === 0) {
3362
+ logger.error("No URLs provided. Pass URLs as args, --file, --from-sitemap, or stdin.");
3363
+ process.exit(1);
3364
+ }
3365
+ const ctx = await createCommandContext({ needsAuth: true });
3366
+ const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
3367
+ const delayMs = Number.parseInt(String(args["delay-ms"]), 10);
3368
+ const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 1);
3369
+ if (!quiet) logger.info(`Inspecting ${urls.length} URLs ...`);
3370
+ const results = await batchInspectUrls(ctx.client, siteUrl, urls, {
3371
+ delayMs,
3372
+ concurrency,
3373
+ onProgress: quiet ? void 0 : (r, i, total) => logger.info(`[${i + 1}/${total}] ${r.url} ${r.isIndexed ? "PASS" : "FAIL"}`)
3374
+ }).catch(gscErrorHandler);
3375
+ if (json) {
3376
+ const flattened = results.map((r) => {
3377
+ const indexStatus = r.inspection?.indexStatusResult;
3378
+ return {
3379
+ url: r.url,
3380
+ verdict: indexStatus?.verdict || null,
3381
+ coverageState: indexStatus?.coverageState || null,
3382
+ indexingState: indexStatus?.indexingState || null,
3383
+ lastCrawlTime: indexStatus?.lastCrawlTime || null,
3384
+ isIndexed: r.isIndexed,
3385
+ raw: r.inspection
3386
+ };
3387
+ });
3388
+ console.log(JSON.stringify(flattened, null, 2));
3389
+ return;
3390
+ }
3391
+ const indexed = results.filter((r) => r.isIndexed).length;
3392
+ if (!quiet) logger.success(`Inspected ${results.length} URLs (${indexed} indexed, ${results.length - indexed} not)`);
3393
+ }
3394
+ });
3395
+ const inspectCommand = defineCommand({
3396
+ meta: {
3397
+ name: "inspect",
3398
+ description: "Inspect URL indexing status (single URL; use `inspect batch` for many)"
3399
+ },
3400
+ args: {
3401
+ ...OUTPUT_ARGS,
3402
+ site: {
3403
+ type: "string",
3404
+ alias: "s",
3405
+ description: "Site URL (defaults to config.defaultSite or prompt)"
3020
3406
  },
3021
- async run({ args }) {
3022
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
3023
- const urls = await readUrlList$1(args);
3024
- if (urls.length === 0) {
3025
- logger.error("No URLs provided. Pass URLs as args, --file, or stdin.");
3026
- process.exit(1);
3027
- }
3028
- const ctx = await createCommandContext({ needsAuth: true });
3029
- const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
3030
- const delayMs = Number.parseInt(String(args["delay-ms"]), 10);
3031
- const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 1);
3032
- if (!args.json && !args.quiet) logger.info(`Inspecting ${urls.length} URLs ...`);
3033
- const results = await batchInspectUrls(ctx.client, siteUrl, urls, {
3034
- delayMs,
3035
- concurrency,
3036
- onProgress: args.json || args.quiet ? void 0 : (r, i, total) => logger.info(`[${i + 1}/${total}] ${r.url} ${r.isIndexed ? "PASS" : "FAIL"}`)
3037
- }).catch(gscErrorHandler);
3038
- if (args.json) {
3039
- console.log(JSON.stringify(results, null, 2));
3040
- return;
3041
- }
3042
- const indexed = results.filter((r) => r.isIndexed).length;
3043
- if (!args.quiet) logger.success(`Inspected ${results.length} URLs (${indexed} indexed, ${results.length - indexed} not)`);
3407
+ url: {
3408
+ type: "positional",
3409
+ required: true,
3410
+ description: "URL to inspect"
3044
3411
  }
3045
- }) },
3412
+ },
3413
+ subCommands: { batch: batchCommand },
3046
3414
  async run({ args }) {
3047
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
3415
+ const { json } = applyOutputMode(args);
3048
3416
  const ctx = await createCommandContext({ needsAuth: true });
3049
3417
  const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
3050
3418
  const result = await ctx.client.inspect(siteUrl, args.url).catch(gscErrorHandler);
3051
3419
  const inspection = result?.inspectionResult;
3052
3420
  const indexStatus = inspection?.indexStatusResult;
3053
- if (args.json) {
3421
+ if (json) {
3054
3422
  console.log(JSON.stringify({
3055
3423
  url: args.url,
3056
3424
  verdict: indexStatus?.verdict || null,
@@ -3622,6 +3990,222 @@ async function writeOutput(opts) {
3622
3990
  function isKnownTable$1(name) {
3623
3991
  return allTables().includes(name);
3624
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
+ });
3625
4209
  const sitemapsCommand = defineCommand({
3626
4210
  meta: {
3627
4211
  name: "sitemaps",
@@ -3634,16 +4218,12 @@ const sitemapsCommand = defineCommand({
3634
4218
  description: "List sitemaps for a site"
3635
4219
  },
3636
4220
  args: {
4221
+ ...OUTPUT_ARGS,
3637
4222
  site: {
3638
4223
  type: "string",
3639
4224
  alias: "s",
3640
4225
  description: "Site URL (e.g., sc-domain:example.com or https://example.com/)"
3641
4226
  },
3642
- json: {
3643
- type: "boolean",
3644
- default: false,
3645
- description: "Output as JSON"
3646
- },
3647
4227
  pending: {
3648
4228
  type: "boolean",
3649
4229
  default: false,
@@ -3656,12 +4236,10 @@ const sitemapsCommand = defineCommand({
3656
4236
  }
3657
4237
  },
3658
4238
  async run({ args }) {
4239
+ const { json } = applyOutputMode(args);
3659
4240
  const ctx = await createCommandContext({ needsAuth: true });
3660
4241
  const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
3661
- let sitemaps = (await ctx.client.sitemaps.list(siteUrl).catch((e) => {
3662
- logger.error(`Failed to fetch sitemaps: ${e.message}`);
3663
- process.exit(1);
3664
- })).map((sm) => ({
4242
+ let sitemaps = (await ctx.client.sitemaps.list(siteUrl).catch(gscErrorHandler)).map((sm) => ({
3665
4243
  path: sm.path,
3666
4244
  type: sm.type || void 0,
3667
4245
  isPending: sm.isPending || false,
@@ -3672,7 +4250,7 @@ const sitemapsCommand = defineCommand({
3672
4250
  }));
3673
4251
  if (args.pending) sitemaps = sitemaps.filter((sm) => sm.isPending);
3674
4252
  if (args.errored) sitemaps = sitemaps.filter((sm) => sm.errors > 0);
3675
- if (args.json) {
4253
+ if (json) {
3676
4254
  console.log(JSON.stringify(sitemaps, null, 2));
3677
4255
  return;
3678
4256
  }
@@ -3697,6 +4275,7 @@ const sitemapsCommand = defineCommand({
3697
4275
  description: "Get details for a specific sitemap"
3698
4276
  },
3699
4277
  args: {
4278
+ ...OUTPUT_ARGS,
3700
4279
  site: {
3701
4280
  type: "string",
3702
4281
  alias: "s",
@@ -3706,19 +4285,15 @@ const sitemapsCommand = defineCommand({
3706
4285
  type: "positional",
3707
4286
  required: true,
3708
4287
  description: "Sitemap URL"
3709
- },
3710
- json: {
3711
- type: "boolean",
3712
- default: false,
3713
- description: "Output as JSON"
3714
4288
  }
3715
4289
  },
3716
4290
  async run({ args }) {
4291
+ const { json } = applyOutputMode(args);
3717
4292
  const ctx = await createCommandContext({ needsAuth: true });
3718
4293
  const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
3719
4294
  const client = ctx.client;
3720
4295
  const sitemap = await fetchSitemap(client, siteUrl, args.url).catch(gscErrorHandler);
3721
- if (args.json) {
4296
+ if (json) {
3722
4297
  console.log(JSON.stringify(sitemap, null, 2));
3723
4298
  return;
3724
4299
  }
@@ -3743,6 +4318,7 @@ const sitemapsCommand = defineCommand({
3743
4318
  description: "Submit a sitemap to GSC"
3744
4319
  },
3745
4320
  args: {
4321
+ ...OUTPUT_ARGS,
3746
4322
  site: {
3747
4323
  type: "string",
3748
4324
  alias: "s",
@@ -3755,12 +4331,18 @@ const sitemapsCommand = defineCommand({
3755
4331
  }
3756
4332
  },
3757
4333
  async run({ args }) {
4334
+ const { json } = applyOutputMode(args);
3758
4335
  const ctx = await createCommandContext({ needsAuth: true });
3759
4336
  const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
3760
- await ctx.client.sitemaps.submit(siteUrl, args.url).catch((e) => {
3761
- logger.error(`Submit failed: ${e.message}`);
3762
- process.exit(1);
3763
- });
4337
+ await ctx.client.sitemaps.submit(siteUrl, args.url).catch(gscErrorHandler);
4338
+ if (json) {
4339
+ console.log(JSON.stringify({
4340
+ siteUrl,
4341
+ feedpath: args.url,
4342
+ status: "submitted"
4343
+ }, null, 2));
4344
+ return;
4345
+ }
3764
4346
  logger.success(`Submitted sitemap: ${args.url}`);
3765
4347
  }
3766
4348
  }),
@@ -3770,6 +4352,7 @@ const sitemapsCommand = defineCommand({
3770
4352
  description: "Delete a sitemap from GSC"
3771
4353
  },
3772
4354
  args: {
4355
+ ...OUTPUT_ARGS,
3773
4356
  site: {
3774
4357
  type: "string",
3775
4358
  alias: "s",
@@ -3782,13 +4365,94 @@ const sitemapsCommand = defineCommand({
3782
4365
  }
3783
4366
  },
3784
4367
  async run({ args }) {
4368
+ const { json } = applyOutputMode(args);
3785
4369
  const ctx = await createCommandContext({ needsAuth: true });
3786
4370
  const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
3787
- await ctx.client.sitemaps.delete(siteUrl, args.url).catch((e) => {
3788
- logger.error(`Delete failed: ${e.message}`);
4371
+ await ctx.client.sitemaps.delete(siteUrl, args.url).catch(gscErrorHandler);
4372
+ if (json) {
4373
+ console.log(JSON.stringify({
4374
+ siteUrl,
4375
+ feedpath: args.url,
4376
+ status: "deleted"
4377
+ }, null, 2));
4378
+ return;
4379
+ }
4380
+ logger.success(`Deleted sitemap: ${args.url}`);
4381
+ }
4382
+ }),
4383
+ discover: defineCommand({
4384
+ meta: {
4385
+ name: "discover",
4386
+ description: "Probe a domain's robots.txt + common paths for an advertised sitemap (no auth needed)"
4387
+ },
4388
+ args: {
4389
+ ...OUTPUT_ARGS,
4390
+ domain: {
4391
+ type: "positional",
4392
+ required: true,
4393
+ description: "Domain (e.g., example.com)"
4394
+ }
4395
+ },
4396
+ async run({ args }) {
4397
+ const { json } = applyOutputMode(args);
4398
+ const domain = String(args.domain).replace(/^https?:\/\//, "").replace(/\/.*$/, "");
4399
+ const url = await discoverSitemap(domain).catch(() => null);
4400
+ if (json) {
4401
+ console.log(JSON.stringify({
4402
+ domain,
4403
+ sitemap: url
4404
+ }, null, 2));
4405
+ return;
4406
+ }
4407
+ if (!url) {
4408
+ logger.warn(`No sitemap discovered for ${domain}`);
4409
+ process.exit(1);
4410
+ }
4411
+ logger.success(`Discovered sitemap: ${url}`);
4412
+ }
4413
+ }),
4414
+ urls: defineCommand({
4415
+ meta: {
4416
+ name: "urls",
4417
+ description: "Fetch a sitemap (or sitemap index) and dump its <loc> URLs (no auth needed)"
4418
+ },
4419
+ args: {
4420
+ ...OUTPUT_ARGS,
4421
+ "url": {
4422
+ type: "positional",
4423
+ required: true,
4424
+ description: "Sitemap URL (index files are followed)"
4425
+ },
4426
+ "limit": {
4427
+ type: "string",
4428
+ alias: "l",
4429
+ description: "Stop after N URLs across all nested sitemaps"
4430
+ },
4431
+ "max-depth": {
4432
+ type: "string",
4433
+ description: "Max sitemap-index nesting depth (default: 3)"
4434
+ }
4435
+ },
4436
+ async run({ args }) {
4437
+ const { json } = applyOutputMode(args);
4438
+ const limit = args.limit ? Number.parseInt(String(args.limit), 10) : void 0;
4439
+ const maxDepth = args["max-depth"] ? Number.parseInt(String(args["max-depth"]), 10) : void 0;
4440
+ const urls = await fetchSitemapUrls(String(args.url), {
4441
+ limit,
4442
+ maxDepth
4443
+ }).catch((e) => {
4444
+ logger.error(`Sitemap fetch failed: ${e.message}`);
3789
4445
  process.exit(1);
3790
4446
  });
3791
- logger.success(`Deleted sitemap: ${args.url}`);
4447
+ if (json) {
4448
+ console.log(JSON.stringify({
4449
+ sitemap: args.url,
4450
+ count: urls.length,
4451
+ urls
4452
+ }, null, 2));
4453
+ return;
4454
+ }
4455
+ for (const u of urls) console.log(u);
3792
4456
  }
3793
4457
  })
3794
4458
  }
@@ -3847,281 +4511,442 @@ function printPlacementInstructions(method, siteUrl, token) {
3847
4511
  break;
3848
4512
  }
3849
4513
  case "ANALYTICS":
3850
- console.log(` Make sure your Google Analytics tracking tag is installed on the site, then run \`gscdump sites verify\`.`);
4514
+ console.log(` Make sure the Google Analytics tracking tag is installed on the site.`);
4515
+ console.log(` Expected tracking ID:`);
4516
+ console.log();
4517
+ console.log(` \x1B[2m${token}\x1B[0m`);
3851
4518
  break;
3852
4519
  case "TAG_MANAGER":
3853
- console.log(` Make sure your Google Tag Manager container snippet is installed on the site, then run \`gscdump sites verify\`.`);
4520
+ console.log(` Make sure the Google Tag Manager container snippet is installed on the site.`);
4521
+ console.log(` Expected container ID:`);
4522
+ console.log();
4523
+ console.log(` \x1B[2m${token}\x1B[0m`);
3854
4524
  break;
3855
4525
  }
3856
4526
  console.log();
3857
4527
  console.log(` \x1B[90mThen run:\x1B[0m gscdump sites verify ${siteUrl} --method ${method}`);
3858
4528
  console.log();
3859
4529
  }
3860
- const sitesCommand = defineCommand({
4530
+ const addCommand = defineCommand({
3861
4531
  meta: {
3862
- name: "sites",
3863
- description: "List GSC sites; manage properties (add/delete) and verify ownership"
4532
+ name: "add",
4533
+ description: "Register a property in Search Console (pass --verify to chain token + verify in one call)"
3864
4534
  },
3865
4535
  args: {
3866
- "json": {
3867
- type: "boolean",
3868
- default: false,
3869
- description: "Output as JSON for scripting"
4536
+ url: {
4537
+ type: "positional",
4538
+ required: true,
4539
+ description: "Property URL (https://example.com/ or sc-domain:example.com)"
3870
4540
  },
3871
- "with-sitemaps": {
4541
+ verify: {
3872
4542
  type: "boolean",
3873
4543
  default: false,
3874
- description: "Include sitemaps for each owned site"
4544
+ description: "After adding, fetch a verification token and trigger Google's validation"
3875
4545
  },
3876
- "owner-only": {
3877
- type: "boolean",
3878
- default: false,
3879
- description: "Filter to permissionLevel=siteOwner"
4546
+ method: {
4547
+ type: "string",
4548
+ alias: "m",
4549
+ description: "Verification method (used with --verify; default: META for URL-prefix, DNS_TXT for sc-domain:)"
3880
4550
  },
3881
- "quiet": {
4551
+ ...OUTPUT_ARGS
4552
+ },
4553
+ async run({ args }) {
4554
+ applyOutputMode(args);
4555
+ const ctx = await createCommandContext({ needsAuth: true });
4556
+ await addSite(ctx.client, args.url).catch(gscErrorHandler);
4557
+ if (!args.verify) {
4558
+ if (args.json) {
4559
+ console.log(JSON.stringify({
4560
+ siteUrl: args.url,
4561
+ status: "added",
4562
+ verified: false
4563
+ }, null, 2));
4564
+ return;
4565
+ }
4566
+ logger.success(`Added: ${args.url}`);
4567
+ logger.info(`Property is in unverified state. Verify ownership next:`);
4568
+ const method = pickDefaultMethod(args.url);
4569
+ console.log(` \x1B[2mgscdump sites verify-token ${args.url} --method ${method}\x1B[0m`);
4570
+ return;
4571
+ }
4572
+ const method = validateMethod(args.url, args.method ?? pickDefaultMethod(args.url));
4573
+ const tokenResult = await getVerificationToken(ctx.client, args.url, method).catch(gscErrorHandler);
4574
+ if (args.json) {
4575
+ console.log(JSON.stringify({
4576
+ siteUrl: args.url,
4577
+ status: "added",
4578
+ method,
4579
+ token: tokenResult.token,
4580
+ site: tokenResult.site,
4581
+ verified: false,
4582
+ next: "Place the token, then run `gscdump sites verify <url> --method <m>`"
4583
+ }, null, 2));
4584
+ return;
4585
+ }
4586
+ logger.success(`Added: ${args.url}`);
4587
+ printPlacementInstructions(method, args.url, tokenResult.token);
4588
+ const ok = await confirm({
4589
+ message: "Token placed? Trigger Google verification now?",
4590
+ initialValue: true
4591
+ });
4592
+ if (isCancel(ok) || !ok) {
4593
+ logger.info("Skipped verification. Run `gscdump sites verify` once the token is live.");
4594
+ return;
4595
+ }
4596
+ const resource = await verifySite(ctx.client, args.url, method).catch(gscErrorHandler);
4597
+ logger.success(`Verified: ${args.url}`);
4598
+ if (resource.owners?.length) {
4599
+ console.log();
4600
+ console.log(` Owners:`);
4601
+ for (const o of resource.owners) console.log(` \x1B[90m└─\x1B[0m ${o}`);
4602
+ }
4603
+ }
4604
+ });
4605
+ const deleteCommand = defineCommand({
4606
+ meta: {
4607
+ name: "delete",
4608
+ description: "Remove a property from Search Console"
4609
+ },
4610
+ args: {
4611
+ url: {
4612
+ type: "positional",
4613
+ required: true,
4614
+ description: "Property URL to remove"
4615
+ },
4616
+ yes: {
3882
4617
  type: "boolean",
3883
- alias: "q",
4618
+ alias: "y",
3884
4619
  default: false,
3885
- description: "Suppress info/success output"
3886
- }
4620
+ description: "Skip confirmation prompt"
4621
+ },
4622
+ ...OUTPUT_ARGS
3887
4623
  },
3888
- subCommands: {
3889
- "add": defineCommand({
3890
- meta: {
3891
- name: "add",
3892
- description: "Register a property in Search Console (unverified state verify ownership separately)"
3893
- },
3894
- args: {
3895
- url: {
3896
- type: "positional",
3897
- required: true,
3898
- description: "Property URL (https://example.com/ or sc-domain:example.com)"
3899
- },
3900
- json: {
3901
- type: "boolean",
3902
- default: false,
3903
- description: "Output as JSON"
3904
- },
3905
- quiet: {
3906
- type: "boolean",
3907
- alias: "q",
3908
- default: false,
3909
- description: "Suppress info/success output"
3910
- }
3911
- },
3912
- async run({ args }) {
3913
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
3914
- await addSite((await createCommandContext({ needsAuth: true })).client, args.url).catch(gscErrorHandler);
3915
- if (args.json) {
3916
- console.log(JSON.stringify({
3917
- siteUrl: args.url,
3918
- status: "added",
3919
- verified: false
3920
- }, null, 2));
3921
- return;
3922
- }
3923
- logger.success(`Added: ${args.url}`);
3924
- logger.info(`Property is in unverified state. Verify ownership next:`);
3925
- const method = pickDefaultMethod(args.url);
3926
- console.log(` \x1B[2mgscdump sites verify-token ${args.url} --method ${method}\x1B[0m`);
3927
- }
3928
- }),
3929
- "delete": defineCommand({
3930
- meta: {
3931
- name: "delete",
3932
- description: "Remove a property from Search Console"
3933
- },
3934
- args: {
3935
- url: {
3936
- type: "positional",
3937
- required: true,
3938
- description: "Property URL to remove"
3939
- },
3940
- yes: {
3941
- type: "boolean",
3942
- alias: "y",
3943
- default: false,
3944
- description: "Skip confirmation prompt"
3945
- },
3946
- json: {
3947
- type: "boolean",
3948
- default: false,
3949
- description: "Output as JSON"
3950
- },
3951
- quiet: {
3952
- type: "boolean",
3953
- alias: "q",
3954
- default: false,
3955
- description: "Suppress info/success output"
3956
- }
3957
- },
3958
- async run({ args }) {
3959
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
3960
- if (!args.yes && !args.json) {
3961
- const ok = await confirm({
3962
- message: `Remove ${args.url} from Search Console? Local synced data is unaffected.`,
3963
- initialValue: false
3964
- });
3965
- if (isCancel(ok) || !ok) {
3966
- logger.info("Cancelled");
3967
- process.exit(0);
3968
- }
3969
- }
3970
- await deleteSite((await createCommandContext({ needsAuth: true })).client, args.url).catch(gscErrorHandler);
3971
- if (args.json) {
3972
- console.log(JSON.stringify({
3973
- siteUrl: args.url,
3974
- status: "deleted"
3975
- }, null, 2));
3976
- return;
3977
- }
3978
- logger.success(`Removed: ${args.url}`);
3979
- }
3980
- }),
3981
- "verify-token": defineCommand({
3982
- meta: {
3983
- name: "verify-token",
3984
- description: "Get a verification token to place on the site or in DNS"
3985
- },
3986
- args: {
3987
- url: {
3988
- type: "positional",
3989
- required: true,
3990
- description: "Property URL"
3991
- },
3992
- method: {
3993
- type: "string",
3994
- alias: "m",
3995
- description: "META, FILE, DNS_TXT, DNS_CNAME, ANALYTICS, TAG_MANAGER (default: META for URL-prefix, DNS_TXT for sc-domain:)"
3996
- },
3997
- json: {
3998
- type: "boolean",
3999
- default: false,
4000
- description: "Output as JSON"
4001
- },
4002
- quiet: {
4003
- type: "boolean",
4004
- alias: "q",
4005
- default: false,
4006
- description: "Suppress info/success output"
4007
- }
4008
- },
4009
- async run({ args }) {
4010
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
4011
- const method = validateMethod(args.url, args.method ?? pickDefaultMethod(args.url));
4012
- const result = await getVerificationToken((await createCommandContext({ needsAuth: true })).client, args.url, method).catch(gscErrorHandler);
4013
- if (args.json) {
4014
- console.log(JSON.stringify({
4015
- siteUrl: args.url,
4016
- method,
4017
- token: result.token,
4018
- site: result.site
4019
- }, null, 2));
4020
- return;
4021
- }
4022
- printPlacementInstructions(method, args.url, result.token);
4624
+ async run({ args }) {
4625
+ applyOutputMode(args);
4626
+ if (!args.yes && !args.json) {
4627
+ const ok = await confirm({
4628
+ message: `Remove ${args.url} from Search Console? Local synced data is unaffected.`,
4629
+ initialValue: false
4630
+ });
4631
+ if (isCancel(ok) || !ok) {
4632
+ logger.info("Cancelled");
4633
+ process.exit(0);
4023
4634
  }
4024
- }),
4025
- "verify": defineCommand({
4026
- meta: {
4027
- name: "verify",
4028
- description: "Trigger verification — Google fetches/validates the token you placed"
4029
- },
4030
- args: {
4031
- url: {
4032
- type: "positional",
4033
- required: true,
4034
- description: "Property URL"
4035
- },
4036
- method: {
4037
- type: "string",
4038
- alias: "m",
4039
- description: "Verification method to validate (must match the one used for verify-token)"
4040
- },
4041
- json: {
4042
- type: "boolean",
4043
- default: false,
4044
- description: "Output as JSON"
4045
- },
4046
- quiet: {
4047
- type: "boolean",
4048
- alias: "q",
4049
- default: false,
4050
- description: "Suppress info/success output"
4051
- }
4052
- },
4053
- async run({ args }) {
4054
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
4055
- const method = validateMethod(args.url, args.method ?? pickDefaultMethod(args.url));
4056
- const resource = await verifySite((await createCommandContext({ needsAuth: true })).client, args.url, method).catch(gscErrorHandler);
4057
- if (args.json) {
4058
- console.log(JSON.stringify({
4059
- siteUrl: args.url,
4060
- method,
4061
- resource
4062
- }, null, 2));
4063
- return;
4064
- }
4065
- logger.success(`Verified: ${args.url}`);
4066
- if (resource.owners?.length) {
4067
- console.log();
4068
- console.log(` Owners:`);
4069
- for (const o of resource.owners) console.log(` \x1B[90m└─\x1B[0m ${o}`);
4070
- }
4635
+ }
4636
+ await deleteSite((await createCommandContext({ needsAuth: true })).client, args.url).catch(gscErrorHandler);
4637
+ if (args.json) {
4638
+ console.log(JSON.stringify({
4639
+ siteUrl: args.url,
4640
+ status: "deleted"
4641
+ }, null, 2));
4642
+ return;
4643
+ }
4644
+ logger.success(`Removed: ${args.url}`);
4645
+ }
4646
+ });
4647
+ const verifyTokenCommand = defineCommand({
4648
+ meta: {
4649
+ name: "verify-token",
4650
+ description: "Get a verification token to place on the site or in DNS"
4651
+ },
4652
+ args: {
4653
+ url: {
4654
+ type: "positional",
4655
+ required: true,
4656
+ description: "Property URL"
4657
+ },
4658
+ method: {
4659
+ type: "string",
4660
+ alias: "m",
4661
+ description: "META, FILE, DNS_TXT, DNS_CNAME, ANALYTICS, TAG_MANAGER (default: META for URL-prefix, DNS_TXT for sc-domain:)"
4662
+ },
4663
+ ...OUTPUT_ARGS
4664
+ },
4665
+ async run({ args }) {
4666
+ applyOutputMode(args);
4667
+ const method = validateMethod(args.url, args.method ?? pickDefaultMethod(args.url));
4668
+ const result = await getVerificationToken((await createCommandContext({ needsAuth: true })).client, args.url, method).catch(gscErrorHandler);
4669
+ if (args.json) {
4670
+ console.log(JSON.stringify({
4671
+ siteUrl: args.url,
4672
+ method,
4673
+ token: result.token,
4674
+ site: result.site
4675
+ }, null, 2));
4676
+ return;
4677
+ }
4678
+ printPlacementInstructions(method, args.url, result.token);
4679
+ }
4680
+ });
4681
+ const verifyCommand = defineCommand({
4682
+ meta: {
4683
+ name: "verify",
4684
+ description: "Trigger verification — Google fetches/validates the token you placed"
4685
+ },
4686
+ args: {
4687
+ url: {
4688
+ type: "positional",
4689
+ required: true,
4690
+ description: "Property URL"
4691
+ },
4692
+ method: {
4693
+ type: "string",
4694
+ alias: "m",
4695
+ description: "Verification method to validate (must match the one used for verify-token)"
4696
+ },
4697
+ ...OUTPUT_ARGS
4698
+ },
4699
+ async run({ args }) {
4700
+ applyOutputMode(args);
4701
+ const method = validateMethod(args.url, args.method ?? pickDefaultMethod(args.url));
4702
+ const resource = await verifySite((await createCommandContext({ needsAuth: true })).client, args.url, method).catch(gscErrorHandler);
4703
+ if (args.json) {
4704
+ console.log(JSON.stringify({
4705
+ siteUrl: args.url,
4706
+ method,
4707
+ resource
4708
+ }, null, 2));
4709
+ return;
4710
+ }
4711
+ logger.success(`Verified: ${args.url}`);
4712
+ if (resource.owners?.length) {
4713
+ console.log();
4714
+ console.log(` Owners:`);
4715
+ for (const o of resource.owners) console.log(` \x1B[90m└─\x1B[0m ${o}`);
4716
+ }
4717
+ }
4718
+ });
4719
+ const verifyGetCommand = defineCommand({
4720
+ meta: {
4721
+ name: "verify-get",
4722
+ description: "Get a single verified WebResource by id"
4723
+ },
4724
+ args: {
4725
+ id: {
4726
+ type: "positional",
4727
+ required: true,
4728
+ description: "WebResource id (from `sites verify-list`)"
4729
+ },
4730
+ ...OUTPUT_ARGS
4731
+ },
4732
+ async run({ args }) {
4733
+ applyOutputMode(args);
4734
+ const resource = await getVerifiedSite((await createCommandContext({ needsAuth: true })).client, args.id).catch(gscErrorHandler);
4735
+ if (args.json) {
4736
+ console.log(JSON.stringify(resource, null, 2));
4737
+ return;
4738
+ }
4739
+ const ident = resource.site?.identifier ?? resource.id ?? "?";
4740
+ const type = resource.site?.type === "INET_DOMAIN" ? "domain" : "site";
4741
+ console.log();
4742
+ console.log(` \x1B[1m${ident}\x1B[0m \x1B[90m(${type})\x1B[0m`);
4743
+ console.log(` \x1B[90mid:\x1B[0m ${resource.id ?? "?"}`);
4744
+ if (resource.owners?.length) {
4745
+ console.log(` Owners:`);
4746
+ for (const o of resource.owners) console.log(` \x1B[90m└─\x1B[0m ${o}`);
4747
+ }
4748
+ console.log();
4749
+ }
4750
+ });
4751
+ const unverifyCommand = defineCommand({
4752
+ meta: {
4753
+ name: "unverify",
4754
+ description: "Drop your verified ownership of a WebResource (remove the placed token first!)"
4755
+ },
4756
+ args: {
4757
+ id: {
4758
+ type: "positional",
4759
+ required: true,
4760
+ description: "WebResource id (from `sites verify-list`)"
4761
+ },
4762
+ yes: {
4763
+ type: "boolean",
4764
+ alias: "y",
4765
+ default: false,
4766
+ description: "Skip confirmation prompt"
4767
+ },
4768
+ ...OUTPUT_ARGS
4769
+ },
4770
+ async run({ args }) {
4771
+ applyOutputMode(args);
4772
+ if (!args.yes && !args.json) {
4773
+ const ok = await confirm({
4774
+ message: `Unverify WebResource ${args.id}? Remove any placed verification token first or Google may re-verify.`,
4775
+ initialValue: false
4776
+ });
4777
+ if (isCancel(ok) || !ok) {
4778
+ logger.info("Cancelled");
4779
+ process.exit(0);
4071
4780
  }
4072
- })
4781
+ }
4782
+ await unverifySite((await createCommandContext({ needsAuth: true })).client, args.id).catch(gscErrorHandler);
4783
+ if (args.json) {
4784
+ console.log(JSON.stringify({
4785
+ id: args.id,
4786
+ status: "unverified"
4787
+ }, null, 2));
4788
+ return;
4789
+ }
4790
+ logger.success(`Unverified: ${args.id}`);
4791
+ }
4792
+ });
4793
+ const verifyListCommand = defineCommand({
4794
+ meta: {
4795
+ name: "verify-list",
4796
+ description: "List verified WebResources from the Site Verification API (distinct from Search Console properties)"
4073
4797
  },
4798
+ args: { ...OUTPUT_ARGS },
4074
4799
  async run({ args }) {
4075
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
4076
- const ctx = await createCommandContext({ needsAuth: true });
4077
- const ownerOnly = Boolean(args["owner-only"]);
4078
- if (args["with-sitemaps"]) {
4079
- const all = await fetchSitesWithSitemaps(ctx.client).catch(gscErrorHandler);
4080
- const sites = ownerOnly ? all.filter((s) => s.permissionLevel === "siteOwner") : all;
4800
+ applyOutputMode(args);
4801
+ const resources = await listVerifiedSites((await createCommandContext({ needsAuth: true })).client).catch(gscErrorHandler);
4802
+ if (args.json) {
4803
+ console.log(JSON.stringify(resources, null, 2));
4804
+ return;
4805
+ }
4806
+ if (resources.length === 0) {
4807
+ logger.warn("No verified WebResources found");
4808
+ return;
4809
+ }
4810
+ logger.success(`${resources.length} verified WebResources:`);
4811
+ console.log();
4812
+ for (const r of resources) {
4813
+ const id = r.id ?? "?";
4814
+ const site = r.site;
4815
+ const ident = site?.identifier ?? id;
4816
+ const type = site?.type === "INET_DOMAIN" ? "domain" : "site";
4817
+ console.log(` \x1B[1m${ident}\x1B[0m \x1B[90m(${type})\x1B[0m`);
4818
+ if (r.owners?.length) for (const o of r.owners) console.log(` \x1B[90m└─\x1B[0m ${o}`);
4819
+ }
4820
+ }
4821
+ });
4822
+ const getCommand = defineCommand({
4823
+ meta: {
4824
+ name: "get",
4825
+ description: "Show a single property's permissionLevel from the sites list"
4826
+ },
4827
+ args: {
4828
+ url: {
4829
+ type: "positional",
4830
+ required: true,
4831
+ description: "Property URL"
4832
+ },
4833
+ ...OUTPUT_ARGS
4834
+ },
4835
+ async run({ args }) {
4836
+ applyOutputMode(args);
4837
+ const site = (await (await createCommandContext({ needsAuth: true })).loadSites()).find((s) => s.siteUrl === args.url);
4838
+ if (!site) {
4081
4839
  if (args.json) {
4082
- const enriched = sites.map((s) => ({
4083
- ...s,
4084
- sitemapCounts: {
4085
- total: s.sitemaps.length,
4086
- pending: s.sitemaps.filter((sm) => sm.isPending).length,
4087
- errored: s.sitemaps.filter((sm) => Number(sm.errors) > 0).length
4088
- }
4089
- }));
4090
- console.log(JSON.stringify(enriched, null, 2));
4091
- return;
4092
- }
4093
- if (sites.length === 0) {
4094
- logger.warn(ownerOnly ? "No owned sites found" : "No verified sites found");
4095
- return;
4096
- }
4097
- logger.success(`Found ${sites.length} ${ownerOnly ? "owned" : "verified"} sites:`);
4098
- console.log();
4099
- for (const site of sites) {
4100
- const perm = site.permissionLevel === "siteOwner" ? "\x1B[32m" : "\x1B[90m";
4101
- console.log(` ${site.siteUrl} ${perm}(${site.permissionLevel})\x1B[0m`);
4102
- for (const sm of site.sitemaps) {
4103
- const pending = sm.isPending ? " \x1B[33m(pending)\x1B[0m" : "";
4104
- console.log(` \x1B[90m└─\x1B[0m ${sm.path}${pending}`);
4105
- }
4840
+ console.log(JSON.stringify(null));
4841
+ process.exit(1);
4106
4842
  }
4843
+ logger.error(`Not found: ${args.url}`);
4844
+ process.exit(1);
4845
+ }
4846
+ if (args.json) {
4847
+ console.log(JSON.stringify(site, null, 2));
4107
4848
  return;
4108
4849
  }
4109
- const all = await ctx.loadSites();
4850
+ const perm = site.permissionLevel === "siteOwner" ? "\x1B[32m" : "\x1B[90m";
4851
+ console.log();
4852
+ console.log(` \x1B[1m${site.siteUrl}\x1B[0m`);
4853
+ console.log(` Permission: ${perm}${site.permissionLevel}\x1B[0m`);
4854
+ console.log();
4855
+ }
4856
+ });
4857
+ const LIST_ARGS = {
4858
+ ...OUTPUT_ARGS,
4859
+ "with-sitemaps": {
4860
+ type: "boolean",
4861
+ default: false,
4862
+ description: "Include sitemaps for each owned site"
4863
+ },
4864
+ "owner-only": {
4865
+ type: "boolean",
4866
+ default: false,
4867
+ description: "Filter to permissionLevel=siteOwner"
4868
+ }
4869
+ };
4870
+ async function runListSites(args) {
4871
+ applyOutputMode(args);
4872
+ const ctx = await createCommandContext({ needsAuth: true });
4873
+ const ownerOnly = Boolean(args["owner-only"]);
4874
+ if (args["with-sitemaps"]) {
4875
+ const all = await fetchSitesWithSitemaps(ctx.client).catch(gscErrorHandler);
4110
4876
  const sites = ownerOnly ? all.filter((s) => s.permissionLevel === "siteOwner") : all;
4111
4877
  if (args.json) {
4112
- console.log(JSON.stringify(sites, null, 2));
4878
+ const enriched = sites.map((s) => ({
4879
+ ...s,
4880
+ sitemapCounts: {
4881
+ total: s.sitemaps.length,
4882
+ pending: s.sitemaps.filter((sm) => sm.isPending).length,
4883
+ errored: s.sitemaps.filter((sm) => Number(sm.errors) > 0).length
4884
+ }
4885
+ }));
4886
+ console.log(JSON.stringify(enriched, null, 2));
4113
4887
  return;
4114
4888
  }
4115
4889
  if (sites.length === 0) {
4116
4890
  logger.warn(ownerOnly ? "No owned sites found" : "No verified sites found");
4117
4891
  return;
4118
4892
  }
4119
- logger.success(`Found ${sites.length} ${ownerOnly ? "owned " : ""}sites:`);
4893
+ logger.success(`Found ${sites.length} ${ownerOnly ? "owned" : "verified"} sites:`);
4120
4894
  console.log();
4121
4895
  for (const site of sites) {
4122
4896
  const perm = site.permissionLevel === "siteOwner" ? "\x1B[32m" : "\x1B[90m";
4123
4897
  console.log(` ${site.siteUrl} ${perm}(${site.permissionLevel})\x1B[0m`);
4898
+ for (const sm of site.sitemaps) {
4899
+ const pending = sm.isPending ? " \x1B[33m(pending)\x1B[0m" : "";
4900
+ console.log(` \x1B[90m└─\x1B[0m ${sm.path}${pending}`);
4901
+ }
4124
4902
  }
4903
+ return;
4904
+ }
4905
+ const all = await ctx.loadSites();
4906
+ const sites = ownerOnly ? all.filter((s) => s.permissionLevel === "siteOwner") : all;
4907
+ if (args.json) {
4908
+ console.log(JSON.stringify(sites, null, 2));
4909
+ return;
4910
+ }
4911
+ if (sites.length === 0) {
4912
+ logger.warn(ownerOnly ? "No owned sites found" : "No verified sites found");
4913
+ return;
4914
+ }
4915
+ logger.success(`Found ${sites.length} ${ownerOnly ? "owned " : ""}sites:`);
4916
+ console.log();
4917
+ for (const site of sites) {
4918
+ const perm = site.permissionLevel === "siteOwner" ? "\x1B[32m" : "\x1B[90m";
4919
+ console.log(` ${site.siteUrl} ${perm}(${site.permissionLevel})\x1B[0m`);
4920
+ }
4921
+ }
4922
+ const sitesCommand = defineCommand({
4923
+ meta: {
4924
+ name: "sites",
4925
+ description: "List GSC sites; manage properties (add/delete) and verify ownership"
4926
+ },
4927
+ args: LIST_ARGS,
4928
+ subCommands: {
4929
+ "list": defineCommand({
4930
+ meta: {
4931
+ name: "list",
4932
+ description: "List GSC sites (alias of bare `sites`)"
4933
+ },
4934
+ args: LIST_ARGS,
4935
+ async run({ args }) {
4936
+ await runListSites(args);
4937
+ }
4938
+ }),
4939
+ "add": addCommand,
4940
+ "delete": deleteCommand,
4941
+ "get": getCommand,
4942
+ "verify-token": verifyTokenCommand,
4943
+ "verify": verifyCommand,
4944
+ "verify-list": verifyListCommand,
4945
+ "verify-get": verifyGetCommand,
4946
+ "unverify": unverifyCommand
4947
+ },
4948
+ async run({ args }) {
4949
+ await runListSites(args);
4125
4950
  }
4126
4951
  });
4127
4952
  const compactCommand = defineCommand({
@@ -4152,20 +4977,10 @@ const compactCommand = defineCommand({
4152
4977
  default: false,
4153
4978
  description: "Report tier counts per (table, site) without compacting"
4154
4979
  },
4155
- "json": {
4156
- type: "boolean",
4157
- default: false,
4158
- description: "Output a JSON summary"
4159
- },
4160
- "quiet": {
4161
- type: "boolean",
4162
- alias: "q",
4163
- default: false,
4164
- description: "Suppress progress output"
4165
- }
4980
+ ...OUTPUT_ARGS
4166
4981
  },
4167
4982
  async run({ args }) {
4168
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
4983
+ const { json } = applyOutputMode(args);
4169
4984
  const store = (await createCommandContext({ needsStore: true })).store;
4170
4985
  const siteId = args.site ? store.siteIdFor(String(args.site)) : void 0;
4171
4986
  const dryRun = Boolean(args["dry-run"]);
@@ -4187,7 +5002,7 @@ const compactCommand = defineCommand({
4187
5002
  ...countByTier(group)
4188
5003
  });
4189
5004
  }
4190
- if (args.json) {
5005
+ if (json) {
4191
5006
  console.log(JSON.stringify({
4192
5007
  thresholds,
4193
5008
  plan: report
@@ -4222,7 +5037,7 @@ const compactCommand = defineCommand({
4222
5037
  });
4223
5038
  }
4224
5039
  }
4225
- if (args.json) {
5040
+ if (json) {
4226
5041
  console.log(JSON.stringify({
4227
5042
  thresholds,
4228
5043
  compacted: summary
@@ -4314,20 +5129,10 @@ const exportCommand = defineCommand({
4314
5129
  default: false,
4315
5130
  description: "Overwrite the output file if it already exists"
4316
5131
  },
4317
- json: {
4318
- type: "boolean",
4319
- default: false,
4320
- description: "Output a JSON summary instead of formatted text"
4321
- },
4322
- quiet: {
4323
- type: "boolean",
4324
- alias: "q",
4325
- default: false,
4326
- description: "Suppress info/success output"
4327
- }
5132
+ ...OUTPUT_ARGS
4328
5133
  },
4329
5134
  async run({ args }) {
4330
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
5135
+ const { json } = applyOutputMode(args);
4331
5136
  const store = (await createCommandContext({ needsStore: true })).store;
4332
5137
  const siteId = args.site ? store.siteIdFor(args.site) : void 0;
4333
5138
  const result = await exportToDuckDB({
@@ -4338,7 +5143,7 @@ const exportCommand = defineCommand({
4338
5143
  outPath: args.out,
4339
5144
  force: args.force
4340
5145
  });
4341
- if (args.json) {
5146
+ if (json) {
4342
5147
  console.log(JSON.stringify(result, null, 2));
4343
5148
  return;
4344
5149
  }
@@ -4374,20 +5179,10 @@ const gcCommand = defineCommand({
4374
5179
  default: false,
4375
5180
  description: "List retired manifest entries past the grace window without deleting"
4376
5181
  },
4377
- "json": {
4378
- type: "boolean",
4379
- default: false,
4380
- description: "Output a JSON summary"
4381
- },
4382
- "quiet": {
4383
- type: "boolean",
4384
- alias: "q",
4385
- default: false,
4386
- description: "Suppress progress output"
4387
- }
5182
+ ...OUTPUT_ARGS
4388
5183
  },
4389
5184
  async run({ args }) {
4390
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
5185
+ const { json } = applyOutputMode(args);
4391
5186
  const store = (await createCommandContext({ needsStore: true })).store;
4392
5187
  const siteId = args.site ? store.siteIdFor(String(args.site)) : void 0;
4393
5188
  const graceMs = Number(args["grace-hours"]) * 36e5;
@@ -4408,7 +5203,7 @@ const gcCommand = defineCommand({
4408
5203
  objectKey: e.objectKey
4409
5204
  });
4410
5205
  }
4411
- if (args.json) {
5206
+ if (json) {
4412
5207
  console.log(JSON.stringify({
4413
5208
  graceHours: Number(args["grace-hours"]),
4414
5209
  candidates
@@ -4425,7 +5220,7 @@ const gcCommand = defineCommand({
4425
5220
  userId: store.userId,
4426
5221
  siteId
4427
5222
  }, graceMs);
4428
- if (args.json) {
5223
+ if (json) {
4429
5224
  console.log(JSON.stringify({
4430
5225
  graceHours: Number(args["grace-hours"]),
4431
5226
  deleted: result.deleted
@@ -4446,25 +5241,15 @@ const rollupsCommand = defineCommand({
4446
5241
  description: "Rebuild post-sync rollups (daily totals, weekly totals, top-N tables) for a site"
4447
5242
  },
4448
5243
  args: {
5244
+ ...OUTPUT_ARGS,
4449
5245
  site: {
4450
5246
  type: "string",
4451
5247
  alias: "s",
4452
5248
  description: "Restrict to a single site (default: all sites with local data)"
4453
- },
4454
- json: {
4455
- type: "boolean",
4456
- default: false,
4457
- description: "Output a JSON summary"
4458
- },
4459
- quiet: {
4460
- type: "boolean",
4461
- alias: "q",
4462
- default: false,
4463
- description: "Suppress progress output"
4464
5249
  }
4465
5250
  },
4466
5251
  async run({ args }) {
4467
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
5252
+ const { json } = applyOutputMode(args);
4468
5253
  const store = (await createCommandContext({ needsStore: true })).store;
4469
5254
  const explicitSiteId = args.site ? store.siteIdFor(String(args.site)) : void 0;
4470
5255
  const allSiteIds = /* @__PURE__ */ new Set();
@@ -4477,7 +5262,7 @@ const rollupsCommand = defineCommand({
4477
5262
  for (const e of entries) if (e.siteId) allSiteIds.add(e.siteId);
4478
5263
  }
4479
5264
  if (allSiteIds.size === 0) {
4480
- if (args.json) console.log(JSON.stringify({
5265
+ if (json) console.log(JSON.stringify({
4481
5266
  sites: [],
4482
5267
  totalBytes: 0
4483
5268
  }, null, 2));
@@ -4508,11 +5293,11 @@ const rollupsCommand = defineCommand({
4508
5293
  bytes: r.bytes,
4509
5294
  objectKey: r.objectKey
4510
5295
  });
4511
- if (!args.json) console.log(` ${r.id.padEnd(20)} ${(r.bytes / 1024).toFixed(1).padStart(8)} KB ${r.objectKey}`);
5296
+ if (!json) console.log(` ${r.id.padEnd(20)} ${(r.bytes / 1024).toFixed(1).padStart(8)} KB ${r.objectKey}`);
4512
5297
  }
4513
5298
  summary.push(site);
4514
5299
  }
4515
- if (args.json) {
5300
+ if (json) {
4516
5301
  console.log(JSON.stringify({
4517
5302
  sites: summary,
4518
5303
  totalBytes
@@ -4529,24 +5314,14 @@ const statsCommand = defineCommand({
4529
5314
  description: "Show row/byte counts per table and on-disk footprint"
4530
5315
  },
4531
5316
  args: {
4532
- json: {
4533
- type: "boolean",
4534
- default: false,
4535
- description: "Output as JSON"
4536
- },
5317
+ ...OUTPUT_ARGS,
4537
5318
  site: {
4538
5319
  type: "string",
4539
5320
  description: "Limit to one site URL (sc-domain:example.com, https://example.com/, ...)"
4540
- },
4541
- quiet: {
4542
- type: "boolean",
4543
- alias: "q",
4544
- default: false,
4545
- description: "Suppress info/success output"
4546
5321
  }
4547
5322
  },
4548
5323
  async run({ args }) {
4549
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
5324
+ const { json } = applyOutputMode(args);
4550
5325
  const store = (await createCommandContext({ needsStore: true })).store;
4551
5326
  let siteId;
4552
5327
  if (args.site) {
@@ -4578,7 +5353,7 @@ const statsCommand = defineCommand({
4578
5353
  files: 0,
4579
5354
  bytes: 0
4580
5355
  }));
4581
- if (args.json) {
5356
+ if (json) {
4582
5357
  const payload = {
4583
5358
  dataDir: store.dataDir,
4584
5359
  disk,
@@ -4664,11 +5439,98 @@ const storeCommand = defineCommand({
4664
5439
  description: "Manage the local DuckDB/Parquet store"
4665
5440
  },
4666
5441
  subCommands: {
4667
- stats: statsCommand,
4668
- compact: compactCommand,
4669
- gc: gcCommand,
4670
- export: exportCommand,
4671
- rollups: rollupsCommand
5442
+ "stats": statsCommand,
5443
+ "compact": compactCommand,
5444
+ "gc": gcCommand,
5445
+ "export": exportCommand,
5446
+ "rollups": rollupsCommand,
5447
+ "rm-site": defineCommand({
5448
+ meta: {
5449
+ name: "rm-site",
5450
+ description: "Delete every parquet, manifest, watermark, and sync-state record for a single site"
5451
+ },
5452
+ args: {
5453
+ site: {
5454
+ type: "positional",
5455
+ required: true,
5456
+ description: "Site URL (e.g. sc-domain:example.com)"
5457
+ },
5458
+ yes: {
5459
+ type: "boolean",
5460
+ alias: "y",
5461
+ default: false,
5462
+ description: "Skip confirmation prompt"
5463
+ },
5464
+ ...OUTPUT_ARGS
5465
+ },
5466
+ async run({ args }) {
5467
+ const { json } = applyOutputMode(args);
5468
+ const store = (await createCommandContext({ needsStore: true })).store;
5469
+ const siteId = store.siteIdFor(String(args.site));
5470
+ if (!args.yes && !json) {
5471
+ const ok = await confirm({
5472
+ message: `Delete ALL local data for ${args.site}? This is irreversible.`,
5473
+ initialValue: false
5474
+ });
5475
+ if (isCancel(ok) || !ok) {
5476
+ logger.info("Cancelled");
5477
+ process.exit(0);
5478
+ }
5479
+ }
5480
+ const result = await store.engine.purgeTenant({
5481
+ userId: store.userId,
5482
+ siteId
5483
+ });
5484
+ if (json) {
5485
+ console.log(JSON.stringify(result, null, 2));
5486
+ return;
5487
+ }
5488
+ logger.success(`Removed local data for ${args.site}`);
5489
+ console.log(` Objects deleted: ${result.objectsDeleted}`);
5490
+ console.log(` Manifest entries: ${result.entriesRemoved}`);
5491
+ console.log(` Watermarks: ${result.watermarksRemoved}`);
5492
+ console.log(` Sync states: ${result.syncStatesRemoved}`);
5493
+ }
5494
+ }),
5495
+ "reset": defineCommand({
5496
+ meta: {
5497
+ name: "reset",
5498
+ description: "Wipe the entire local store (every site, every table). Irreversible."
5499
+ },
5500
+ args: {
5501
+ yes: {
5502
+ type: "boolean",
5503
+ alias: "y",
5504
+ default: false,
5505
+ description: "Skip confirmation prompt"
5506
+ },
5507
+ ...OUTPUT_ARGS
5508
+ },
5509
+ async run({ args }) {
5510
+ const { json } = applyOutputMode(args);
5511
+ const store = (await createCommandContext({ needsStore: true })).store;
5512
+ if (!args.yes && !json) {
5513
+ const ok = await confirm({
5514
+ message: `Wipe the entire local store under ${store.dataDir}? This deletes data for ALL sites.`,
5515
+ initialValue: false
5516
+ });
5517
+ if (isCancel(ok) || !ok) {
5518
+ logger.info("Cancelled");
5519
+ process.exit(0);
5520
+ }
5521
+ }
5522
+ const result = await store.engine.purgeTenant({ userId: store.userId });
5523
+ if (json) {
5524
+ console.log(JSON.stringify(result, null, 2));
5525
+ return;
5526
+ }
5527
+ logger.success("Local store reset");
5528
+ console.log(` Objects deleted: ${result.objectsDeleted}`);
5529
+ console.log(` Manifest entries: ${result.entriesRemoved}`);
5530
+ console.log(` Watermarks: ${result.watermarksRemoved}`);
5531
+ console.log(` Sync states: ${result.syncStatesRemoved}`);
5532
+ }
5533
+ })
4672
5534
  }
4673
5535
  });
4674
5536
  const DEFAULT_TABLES = [
@@ -4844,12 +5706,7 @@ const syncCommand = defineCommand({
4844
5706
  type: "boolean",
4845
5707
  description: "Sync the last 450 days (full GSC history)"
4846
5708
  },
4847
- "quiet": {
4848
- type: "boolean",
4849
- alias: "q",
4850
- default: false,
4851
- description: "Suppress progress output"
4852
- },
5709
+ ...OUTPUT_ARGS,
4853
5710
  "force": {
4854
5711
  type: "boolean",
4855
5712
  default: false,
@@ -4860,11 +5717,6 @@ const syncCommand = defineCommand({
4860
5717
  default: false,
4861
5718
  description: "Print watermarks + sync-state summary instead of syncing"
4862
5719
  },
4863
- "json": {
4864
- type: "boolean",
4865
- default: false,
4866
- description: "With --status: emit JSON"
4867
- },
4868
5720
  "concurrency": {
4869
5721
  type: "string",
4870
5722
  alias: "c",
@@ -4887,8 +5739,9 @@ const syncCommand = defineCommand({
4887
5739
  }
4888
5740
  },
4889
5741
  async run({ args }) {
5742
+ const { json, quiet } = applyOutputMode(args);
4890
5743
  if (args.status) {
4891
- await printSyncStatus(await loadConfig(), args.site ? String(args.site) : void 0, Boolean(args.json));
5744
+ await printSyncStatus(await loadConfig(), args.site ? String(args.site) : void 0, json);
4892
5745
  return;
4893
5746
  }
4894
5747
  const ctx = await createCommandContext({
@@ -4923,7 +5776,7 @@ const syncCommand = defineCommand({
4923
5776
  logger.warn(`All requested types (${requestedTypes.join(", ")}) are marked empty for this site. Pass --force-types to re-probe.`);
4924
5777
  return;
4925
5778
  }
4926
- if (skippedTypes.length > 0 && !args.quiet) logger.info(`Skipping ${skippedTypes.join(", ")} (marked empty for this site; pass --force-types to re-probe).`);
5779
+ if (skippedTypes.length > 0 && !quiet) logger.info(`Skipping ${skippedTypes.join(", ")} (marked empty for this site; pass --force-types to re-probe).`);
4927
5780
  const endDate = args.end ? String(args.end) : daysAgo(DEFAULT_PENDING_DAYS);
4928
5781
  let startDate;
4929
5782
  if (args.start) startDate = String(args.start);
@@ -4953,7 +5806,7 @@ const syncCommand = defineCommand({
4953
5806
  return;
4954
5807
  }
4955
5808
  args.force = true;
4956
- if (!args.quiet) logger.info(`--retry-failed: ${dates.length} date(s) to retry`);
5809
+ if (!quiet) logger.info(`--retry-failed: ${dates.length} date(s) to retry`);
4957
5810
  }
4958
5811
  if (args["dry-run"]) {
4959
5812
  const plan = [];
@@ -4962,7 +5815,7 @@ const syncCommand = defineCommand({
4962
5815
  searchType: type,
4963
5816
  date
4964
5817
  });
4965
- if (args.json) {
5818
+ if (json) {
4966
5819
  console.log(JSON.stringify({
4967
5820
  siteUrl,
4968
5821
  range: {
@@ -4985,7 +5838,7 @@ const syncCommand = defineCommand({
4985
5838
  logger.info("Pass without --dry-run to execute.");
4986
5839
  return;
4987
5840
  }
4988
- if (!args.quiet) {
5841
+ if (!quiet) {
4989
5842
  logger.info(`Syncing ${siteUrl} (${tables.join(", ")}) [${types.join(", ")}] → ${displayPath(store.dataDir)}`);
4990
5843
  logger.info(`Range: ${startDate} → ${endDate} (${dates.length} days)`);
4991
5844
  }
@@ -5002,7 +5855,7 @@ const syncCommand = defineCommand({
5002
5855
  label
5003
5856
  });
5004
5857
  }
5005
- const progress = createProgressTracker(dates.length * jobs.length, Boolean(args.quiet));
5858
+ const progress = createProgressTracker(dates.length * jobs.length, quiet);
5006
5859
  if (serialTables) for (const job of jobs) totals[job.label] = await syncTable(store, siteUrl, job.table, job.type, dates, client, concurrency, args.force, progress);
5007
5860
  else {
5008
5861
  const results = await Promise.all(jobs.map((job) => syncTable(store, siteUrl, job.table, job.type, dates, client, concurrency, args.force, progress)));
@@ -5012,7 +5865,7 @@ const syncCommand = defineCommand({
5012
5865
  }
5013
5866
  progress.done();
5014
5867
  const seconds = ((Date.now() - start) / 1e3).toFixed(1);
5015
- if (!args.quiet) {
5868
+ if (!quiet) {
5016
5869
  logger.success(`Synced ${siteUrl} in ${seconds}s`);
5017
5870
  for (const [t, n] of Object.entries(totals)) {
5018
5871
  const suffix = [n.skipped > 0 ? `${n.skipped} skipped` : null, n.failed > 0 ? `\x1B[31m${n.failed} failed\x1B[0m` : null].filter(Boolean).join(", ");
@@ -5041,7 +5894,7 @@ const syncCommand = defineCommand({
5041
5894
  userId: store.userId,
5042
5895
  siteId
5043
5896
  }, toMark);
5044
- if (!args.quiet) logger.info(`Marked empty for future syncs: ${toMark.join(", ")} (0 rows across ${dates.length} days; pass --force-types to re-probe).`);
5897
+ if (!quiet) logger.info(`Marked empty for future syncs: ${toMark.join(", ")} (0 rows across ${dates.length} days; pass --force-types to re-probe).`);
5045
5898
  }
5046
5899
  }
5047
5900
  if (forceTypes && emptyTypesDoc.emptyTypes.length > 0) {
@@ -5052,13 +5905,13 @@ const syncCommand = defineCommand({
5052
5905
  userId: store.userId,
5053
5906
  siteId
5054
5907
  }, toClear);
5055
- if (!args.quiet) logger.info(`Cleared empty markers for: ${toClear.join(", ")} (re-probe found data).`);
5908
+ if (!quiet) logger.info(`Cleared empty markers for: ${toClear.join(", ")} (re-probe found data).`);
5056
5909
  }
5057
5910
  }
5058
5911
  const noRollups = Boolean(args["no-rollups"]);
5059
5912
  const anyRowsSynced = Object.values(totals).some((t) => t.rows > 0);
5060
5913
  if (!noRollups && anyRowsSynced) {
5061
- if (!args.quiet) logger.info(`Rebuilding rollups for [${siteId}] (${DEFAULT_ROLLUPS.length} rollups)…`);
5914
+ if (!quiet) logger.info(`Rebuilding rollups for [${siteId}] (${DEFAULT_ROLLUPS.length} rollups)…`);
5062
5915
  const rollupStart = Date.now();
5063
5916
  const results = await rebuildRollups({
5064
5917
  engine: store.engine,
@@ -5072,7 +5925,7 @@ const syncCommand = defineCommand({
5072
5925
  logger.warn(`Rollup rebuild failed: ${err.message}`);
5073
5926
  return [];
5074
5927
  });
5075
- if (!args.quiet && results.length > 0) {
5928
+ if (!quiet && results.length > 0) {
5076
5929
  const kb = results.reduce((a, r) => a + r.bytes, 0) / 1024;
5077
5930
  const ms = Date.now() - rollupStart;
5078
5931
  logger.success(`Rebuilt ${results.length} rollup(s) in ${ms}ms — ${kb.toFixed(1)} KB`);
@@ -5160,10 +6013,11 @@ function shouldShowSplash() {
5160
6013
  function applyGlobalArgs() {
5161
6014
  const argv = process.argv;
5162
6015
  if (argv.includes("--no-color") || process.env.NO_COLOR) setNoColor(true);
5163
- const profile = pluckArgValue(argv, "--profile") ?? process.env.GSCDUMP_PROFILE;
5164
- const configDir = pluckArgValue(argv, "--config-dir") ?? process.env.GSCDUMP_CONFIG_DIR;
5165
- if (configDir) setConfigDir(configDir);
5166
- else if (profile) setConfigDir(path.join(os.homedir(), ".config", "gscdump", "profiles", profile));
6016
+ const profile = pluckArgValue(argv, "--profile");
6017
+ applyProfileFromCli({
6018
+ configDir: pluckArgValue(argv, "--config-dir") ?? process.env.GSCDUMP_CONFIG_DIR ?? null,
6019
+ profile
6020
+ });
5167
6021
  if (argv.includes("-v") && !argv.includes("--version")) {
5168
6022
  const i = argv.indexOf("-v");
5169
6023
  argv[i] = "--version";
@@ -5205,8 +6059,10 @@ runMain(defineCommand({
5205
6059
  indexing: indexingCommand,
5206
6060
  entities: entitiesCommand,
5207
6061
  analyze: analyzeCommand,
6062
+ report: reportCommand,
5208
6063
  auth: authCommand,
5209
6064
  config: configCommand,
6065
+ profile: profileCommand,
5210
6066
  doctor: doctorCommand,
5211
6067
  mcp: mcpCommand
5212
6068
  },