@gscdump/cli 0.8.1 → 0.8.2

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
@@ -2,20 +2,20 @@
2
2
  import { t as __exportAll } from "./_chunks/rolldown-runtime.mjs";
3
3
  import { t as ofetch } from "./_chunks/libs/ofetch.mjs";
4
4
  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";
7
5
  import process from "node:process";
8
6
  import { defineCommand, runMain } from "citty";
9
7
  import { defaultAnalyzerRegistry } from "@gscdump/analysis/registry";
10
8
  import { AnalyzerCapabilityError, analyzeFromSource, createEngineQuerySource } from "@gscdump/analysis";
11
9
  import { createGscApiQuerySource } from "@gscdump/engine-gsc-api";
12
10
  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";
11
+ 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";
14
12
  import fs, { readFile, rm } from "node:fs/promises";
15
13
  import { createServer } from "node:http";
14
+ import path from "node:path";
16
15
  import { JWT, OAuth2Client } from "google-auth-library";
17
16
  import { Buffer } from "node:buffer";
18
17
  import fs$1 from "node:fs";
18
+ import os from "node:os";
19
19
  import { createConsola } from "consola";
20
20
  import { createNodeHarness } from "@gscdump/engine-duckdb-node";
21
21
  import { TABLE_DIMS, transformGscRow } from "@gscdump/engine/ingest";
@@ -110,7 +110,7 @@ function loadEnvFromCwd() {
110
110
  }
111
111
  return applied;
112
112
  }
113
- const VERSION = "0.8.1";
113
+ const VERSION = "0.8.2";
114
114
  const baseLogger = createConsola({
115
115
  stdout: process.stderr,
116
116
  stderr: process.stderr
@@ -119,6 +119,28 @@ const logger = baseLogger.withTag("gscdump");
119
119
  function setQuiet(quiet) {
120
120
  if (quiet) baseLogger.level = 1;
121
121
  }
122
+ const OUTPUT_ARGS = {
123
+ json: {
124
+ type: "boolean",
125
+ default: false,
126
+ description: "Output as JSON"
127
+ },
128
+ quiet: {
129
+ type: "boolean",
130
+ alias: "q",
131
+ default: false,
132
+ description: "Suppress info/success output"
133
+ }
134
+ };
135
+ function applyOutputMode(args) {
136
+ const json = Boolean(args.json);
137
+ const quiet = json || Boolean(args.quiet);
138
+ setQuiet(quiet);
139
+ return {
140
+ json,
141
+ quiet
142
+ };
143
+ }
122
144
  let colorEnabled = (() => {
123
145
  if (process.env.NO_COLOR) return false;
124
146
  if (process.argv.includes("--no-color")) return false;
@@ -257,7 +279,8 @@ async function loadServiceAccount(jsonPath) {
257
279
  });
258
280
  }
259
281
  async function resolveServiceAccount(opts = {}) {
260
- const p = opts.path || process.env.GSC_SERVICE_ACCOUNT_JSON || process.env.GOOGLE_APPLICATION_CREDENTIALS;
282
+ let p = opts.path || process.env.GSC_SERVICE_ACCOUNT_JSON || process.env.GOOGLE_APPLICATION_CREDENTIALS;
283
+ if (!p) p = (await loadConfig().catch(() => null))?.serviceAccountPath;
261
284
  if (!p) return null;
262
285
  return loadServiceAccount(p);
263
286
  }
@@ -538,15 +561,21 @@ function redactCred(v, keepTail = 6) {
538
561
  async function describeAuthProvenance() {
539
562
  const rows = [];
540
563
  const warnings = [];
541
- const saPath = process.env.GSC_SERVICE_ACCOUNT_JSON || process.env.GOOGLE_APPLICATION_CREDENTIALS;
542
- if (saPath) {
564
+ const saEnvPath = process.env.GSC_SERVICE_ACCOUNT_JSON || process.env.GOOGLE_APPLICATION_CREDENTIALS;
565
+ const saConfigPath = !saEnvPath ? (await loadConfig().catch(() => null))?.serviceAccountPath : void 0;
566
+ const saPath = saEnvPath || saConfigPath;
567
+ if (saEnvPath) {
543
568
  const saEnv = process.env.GSC_SERVICE_ACCOUNT_JSON ? "GSC_SERVICE_ACCOUNT_JSON" : "GOOGLE_APPLICATION_CREDENTIALS";
544
569
  rows.push({
545
570
  field: "service_account",
546
571
  source: envSourceLabel(saEnv),
547
- value: displayPath(saPath)
572
+ value: displayPath(saEnvPath)
548
573
  });
549
- }
574
+ } else if (saConfigPath) rows.push({
575
+ field: "service_account",
576
+ source: `${displayPath(`${getConfigDir()}/config.json`)}`,
577
+ value: displayPath(saConfigPath)
578
+ });
550
579
  const clientId = pickEnvSource("GSC_CLIENT_ID", "GOOGLE_CLIENT_ID");
551
580
  const clientSecret = pickEnvSource("GSC_CLIENT_SECRET", "GOOGLE_CLIENT_SECRET");
552
581
  const config = await loadConfig().catch(() => null);
@@ -1036,41 +1065,319 @@ const analyzeCommand = defineCommand({
1036
1065
  name: "analyze",
1037
1066
  description: "SEO analysis tools"
1038
1067
  },
1039
- subCommands: Object.fromEntries(ANALYSIS_TOOLS.map((tool) => [tool, makeToolCommand(tool)]))
1068
+ subCommands: {
1069
+ list: defineCommand({
1070
+ meta: {
1071
+ name: "list",
1072
+ description: "List available analyzer ids"
1073
+ },
1074
+ args: { json: {
1075
+ type: "boolean",
1076
+ default: false,
1077
+ description: "Output as JSON"
1078
+ } },
1079
+ async run({ args }) {
1080
+ if (args.json) {
1081
+ console.log(JSON.stringify(ANALYSIS_TOOLS, null, 2));
1082
+ return;
1083
+ }
1084
+ for (const id of ANALYSIS_TOOLS) console.log(id);
1085
+ }
1086
+ }),
1087
+ ...Object.fromEntries(ANALYSIS_TOOLS.map((tool) => [tool, makeToolCommand(tool)]))
1088
+ }
1089
+ });
1090
+ const ROOT_DIR = path.join(os.homedir(), ".config", "gscdump");
1091
+ const PROFILES_DIR = path.join(ROOT_DIR, "profiles");
1092
+ const ACTIVE_MARKER = path.join(ROOT_DIR, "active-profile");
1093
+ let activeOverride = null;
1094
+ let configDirOverridden = false;
1095
+ function getProfileDir(name) {
1096
+ return path.join(PROFILES_DIR, name);
1097
+ }
1098
+ function readActiveMarkerSync() {
1099
+ if (!fs$1.existsSync(ACTIVE_MARKER)) return null;
1100
+ return fs$1.readFileSync(ACTIVE_MARKER, "utf-8").trim() || null;
1101
+ }
1102
+ function resolveActiveProfile() {
1103
+ return activeOverride ?? process.env.GSCDUMP_PROFILE ?? readActiveMarkerSync();
1104
+ }
1105
+ function applyProfileFromCli(opts) {
1106
+ if (opts.configDir) {
1107
+ setConfigDir(opts.configDir);
1108
+ configDirOverridden = true;
1109
+ return;
1110
+ }
1111
+ if (opts.profile) {
1112
+ activeOverride = opts.profile;
1113
+ setConfigDir(getProfileDir(opts.profile));
1114
+ return;
1115
+ }
1116
+ const envProfile = process.env.GSCDUMP_PROFILE;
1117
+ if (envProfile) {
1118
+ setConfigDir(getProfileDir(envProfile));
1119
+ return;
1120
+ }
1121
+ const marker = readActiveMarkerSync();
1122
+ if (marker) setConfigDir(getProfileDir(marker));
1123
+ }
1124
+ async function setActiveProfile(name) {
1125
+ await fs.mkdir(ROOT_DIR, {
1126
+ recursive: true,
1127
+ mode: 448
1128
+ });
1129
+ if (name == null) {
1130
+ await fs.rm(ACTIVE_MARKER, { force: true }).catch(() => {});
1131
+ return;
1132
+ }
1133
+ await fs.writeFile(ACTIVE_MARKER, name, { mode: 384 });
1134
+ }
1135
+ async function listProfiles() {
1136
+ return fs.readdir(PROFILES_DIR).then((entries) => entries.filter((e) => !e.startsWith(".")).sort()).catch(() => []);
1137
+ }
1138
+ async function createProfile(name) {
1139
+ const dir = getProfileDir(name);
1140
+ await fs.mkdir(dir, {
1141
+ recursive: true,
1142
+ mode: 448
1143
+ });
1144
+ return dir;
1145
+ }
1146
+ function profileNameFromEmail(email) {
1147
+ return email.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1148
+ }
1149
+ async function adoptCurrentConfigAsProfile(name) {
1150
+ if (configDirOverridden) return null;
1151
+ if (resolveActiveProfile()) return null;
1152
+ const currentDir = getConfigDir();
1153
+ const targetDir = getProfileDir(name);
1154
+ if (currentDir === targetDir) return targetDir;
1155
+ await fs.mkdir(targetDir, {
1156
+ recursive: true,
1157
+ mode: 448
1158
+ });
1159
+ for (const f of ["tokens.json", "config.json"]) {
1160
+ const src = path.join(currentDir, f);
1161
+ const dst = path.join(targetDir, f);
1162
+ if (await fs.stat(src).then(() => true).catch(() => false)) await fs.rename(src, dst).catch(() => {});
1163
+ }
1164
+ await setActiveProfile(name);
1165
+ activeOverride = name;
1166
+ setConfigDir(targetDir);
1167
+ return targetDir;
1168
+ }
1169
+ const profileCommand = defineCommand({
1170
+ meta: {
1171
+ name: "profile",
1172
+ description: "Manage gscdump profiles (per-account token + config dirs)"
1173
+ },
1174
+ subCommands: {
1175
+ list: defineCommand({
1176
+ meta: {
1177
+ name: "list",
1178
+ description: "List configured profiles"
1179
+ },
1180
+ args: { ...OUTPUT_ARGS },
1181
+ async run({ args }) {
1182
+ const { json } = applyOutputMode(args);
1183
+ const names = await listProfiles();
1184
+ const active = resolveActiveProfile();
1185
+ if (json) {
1186
+ console.log(JSON.stringify({
1187
+ active,
1188
+ profiles: names
1189
+ }, null, 2));
1190
+ return;
1191
+ }
1192
+ if (names.length === 0) {
1193
+ logger.warn(`No profiles in ${PROFILES_DIR}`);
1194
+ logger.info("Run `gscdump auth login` to create one automatically, or `gscdump profile create <name>`");
1195
+ return;
1196
+ }
1197
+ for (const n of names) console.log(`${n === active ? "*" : " "} ${n}`);
1198
+ }
1199
+ }),
1200
+ path: defineCommand({
1201
+ meta: {
1202
+ name: "path",
1203
+ description: "Print the config directory for a profile"
1204
+ },
1205
+ args: { name: {
1206
+ type: "positional",
1207
+ required: false,
1208
+ description: "Profile name (default: active)"
1209
+ } },
1210
+ async run({ args }) {
1211
+ const name = args.name ? String(args.name) : resolveActiveProfile();
1212
+ if (!name) {
1213
+ logger.error("No profile specified and none active (set --profile, GSCDUMP_PROFILE, or run `gscdump profile use <name>`)");
1214
+ process.exit(1);
1215
+ }
1216
+ console.log(getProfileDir(name));
1217
+ }
1218
+ }),
1219
+ current: defineCommand({
1220
+ meta: {
1221
+ name: "current",
1222
+ description: "Print the active profile name"
1223
+ },
1224
+ async run() {
1225
+ const active = resolveActiveProfile();
1226
+ if (!active) process.exit(1);
1227
+ console.log(active);
1228
+ }
1229
+ }),
1230
+ use: defineCommand({
1231
+ meta: {
1232
+ name: "use",
1233
+ description: "Set the persisted active profile (subsequent commands no longer need --profile)"
1234
+ },
1235
+ args: {
1236
+ ...OUTPUT_ARGS,
1237
+ name: {
1238
+ type: "positional",
1239
+ required: true,
1240
+ description: "Profile name"
1241
+ }
1242
+ },
1243
+ async run({ args }) {
1244
+ applyOutputMode(args);
1245
+ const name = String(args.name);
1246
+ const dir = getProfileDir(name);
1247
+ if (!await fs.stat(dir).then(() => true).catch(() => false)) {
1248
+ logger.error(`Profile not found: ${name}`);
1249
+ logger.info(`Create it with: gscdump profile create ${name}`);
1250
+ process.exit(1);
1251
+ }
1252
+ await setActiveProfile(name);
1253
+ logger.success(`Active profile: ${name}`);
1254
+ }
1255
+ }),
1256
+ create: defineCommand({
1257
+ meta: {
1258
+ name: "create",
1259
+ description: "Create an empty profile directory"
1260
+ },
1261
+ args: {
1262
+ ...OUTPUT_ARGS,
1263
+ "name": {
1264
+ type: "positional",
1265
+ required: true,
1266
+ description: "Profile name"
1267
+ },
1268
+ "no-use": {
1269
+ type: "boolean",
1270
+ default: false,
1271
+ description: "Do not mark the new profile as active"
1272
+ }
1273
+ },
1274
+ async run({ args }) {
1275
+ applyOutputMode(args);
1276
+ const name = String(args.name);
1277
+ const dir = await createProfile(name);
1278
+ if (!args["no-use"]) await setActiveProfile(name);
1279
+ logger.success(`Created profile: ${name}${args["no-use"] ? "" : " (active)"}`);
1280
+ logger.info(displayPath(dir));
1281
+ }
1282
+ }),
1283
+ clear: defineCommand({
1284
+ meta: {
1285
+ name: "clear",
1286
+ description: "Clear the persisted active profile (commands fall back to root config dir)"
1287
+ },
1288
+ args: { ...OUTPUT_ARGS },
1289
+ async run({ args }) {
1290
+ applyOutputMode(args);
1291
+ await setActiveProfile(null);
1292
+ logger.success("Cleared active profile");
1293
+ }
1294
+ }),
1295
+ delete: defineCommand({
1296
+ meta: {
1297
+ name: "delete",
1298
+ description: "Remove a profile directory (tokens + config)"
1299
+ },
1300
+ args: {
1301
+ ...OUTPUT_ARGS,
1302
+ name: {
1303
+ type: "positional",
1304
+ required: true,
1305
+ description: "Profile name"
1306
+ },
1307
+ yes: {
1308
+ type: "boolean",
1309
+ alias: "y",
1310
+ default: false,
1311
+ description: "Skip confirmation"
1312
+ }
1313
+ },
1314
+ async run({ args }) {
1315
+ applyOutputMode(args);
1316
+ const name = String(args.name);
1317
+ const dir = getProfileDir(name);
1318
+ if (!await fs.stat(dir).then(() => true).catch(() => false)) {
1319
+ logger.error(`Profile not found: ${name}`);
1320
+ process.exit(1);
1321
+ }
1322
+ if (!args.yes) {
1323
+ const ok = await confirm({
1324
+ message: `Delete profile "${name}" at ${dir}? Tokens and config will be lost.`,
1325
+ initialValue: false
1326
+ });
1327
+ if (isCancel(ok) || !ok) {
1328
+ logger.info("Cancelled");
1329
+ process.exit(0);
1330
+ }
1331
+ }
1332
+ await fs.rm(dir, {
1333
+ recursive: true,
1334
+ force: true
1335
+ });
1336
+ if (readActiveMarkerSync() === name) await setActiveProfile(null);
1337
+ logger.success(`Removed profile: ${name}`);
1338
+ }
1339
+ })
1340
+ }
1040
1341
  });
1342
+ const REQUIRED_SCOPES$1 = [
1343
+ "https://www.googleapis.com/auth/webmasters",
1344
+ "https://www.googleapis.com/auth/indexing",
1345
+ "https://www.googleapis.com/auth/siteverification"
1346
+ ];
1041
1347
  async function fetchTokenInfo(accessToken) {
1042
1348
  return ofetch("https://oauth2.googleapis.com/tokeninfo", { query: { access_token: accessToken } }).catch(() => null);
1043
1349
  }
1350
+ async function resolveLiveAuthState() {
1351
+ const tokens = await loadTokens();
1352
+ const byok = resolveBYOK();
1353
+ let liveToken = null;
1354
+ if (typeof byok === "string") liveToken = byok;
1355
+ else if (byok && "getAccessToken" in byok) liveToken = await byok.getAccessToken().then((r) => r.token ?? null).catch(() => null);
1356
+ else if (tokens?.access_token) liveToken = tokens.access_token;
1357
+ const tokenInfo = liveToken ? await fetchTokenInfo(liveToken) : null;
1358
+ const scopes = tokenInfo?.scope ? tokenInfo.scope.split(/\s+/).filter(Boolean) : [];
1359
+ const has = (s) => scopes.includes(s) || scopes.includes(s.replace(".readonly", ""));
1360
+ const missing = REQUIRED_SCOPES$1.filter((s) => !has(s));
1361
+ return {
1362
+ byok,
1363
+ tokens,
1364
+ liveToken,
1365
+ tokenInfo,
1366
+ scopes,
1367
+ missing
1368
+ };
1369
+ }
1044
1370
  const statusCommand$1 = defineCommand({
1045
1371
  meta: {
1046
1372
  name: "status",
1047
1373
  description: "Show current authentication status"
1048
1374
  },
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
- },
1375
+ args: { ...OUTPUT_ARGS },
1062
1376
  async run({ args }) {
1063
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
1064
- const tokens = await loadTokens();
1065
- const byok = resolveBYOK();
1377
+ const { json } = applyOutputMode(args);
1378
+ const { byok, tokens, tokenInfo, scopes, missing } = await resolveLiveAuthState();
1066
1379
  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) {
1380
+ if (json) {
1074
1381
  console.log(JSON.stringify({
1075
1382
  authenticated: !!tokens || !!byok,
1076
1383
  source: byok ? "byok" : tokens ? "saved-tokens" : null,
@@ -1089,14 +1396,7 @@ const statusCommand$1 = defineCommand({
1089
1396
  const reportScopes = () => {
1090
1397
  if (scopes.length === 0) return;
1091
1398
  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
1399
  for (const s of scopes) console.log(` \x1B[90m└─\x1B[0m ${s}`);
1099
- const missing = required.filter((s) => !has(s));
1100
1400
  if (missing.length > 0) {
1101
1401
  console.log(` \x1B[33mMissing scopes:\x1B[0m`);
1102
1402
  for (const s of missing) console.log(` \x1B[90m└─\x1B[0m ${s}`);
@@ -1136,14 +1436,9 @@ const refreshCommand = defineCommand({
1136
1436
  name: "refresh",
1137
1437
  description: "Force-refresh saved OAuth tokens (no-op for BYOK)"
1138
1438
  },
1139
- args: { quiet: {
1140
- type: "boolean",
1141
- alias: "q",
1142
- default: false,
1143
- description: "Suppress info/success output"
1144
- } },
1439
+ args: { ...OUTPUT_ARGS },
1145
1440
  async run({ args }) {
1146
- setQuiet(Boolean(args.quiet));
1441
+ applyOutputMode(args);
1147
1442
  if (resolveBYOK()) {
1148
1443
  logger.info("BYOK detected; refresh handled per-call by the SDK");
1149
1444
  return;
@@ -1181,6 +1476,7 @@ const authCommand = defineCommand({
1181
1476
  description: "Run OAuth flow and persist tokens (skip if BYOK env vars set)"
1182
1477
  },
1183
1478
  args: {
1479
+ ...OUTPUT_ARGS,
1184
1480
  "force": {
1185
1481
  type: "boolean",
1186
1482
  alias: "f",
@@ -1195,22 +1491,17 @@ const authCommand = defineCommand({
1195
1491
  "service-account": {
1196
1492
  type: "string",
1197
1493
  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
1494
  }
1205
1495
  },
1206
1496
  async run({ args }) {
1207
- setQuiet(Boolean(args.quiet));
1497
+ applyOutputMode(args);
1208
1498
  if (resolveBYOK() && !args.force) {
1209
1499
  logger.info("BYOK env vars detected, no login needed (--force to override)");
1210
1500
  return;
1211
1501
  }
1212
1502
  if (args["service-account"]) {
1213
- const jwt = await loadServiceAccount(String(args["service-account"])).catch((e) => {
1503
+ const saPath = path.resolve(String(args["service-account"]));
1504
+ const jwt = await loadServiceAccount(saPath).catch((e) => {
1214
1505
  logger.error(`Service-account load failed: ${e.message}`);
1215
1506
  process.exit(1);
1216
1507
  });
@@ -1218,8 +1509,11 @@ const authCommand = defineCommand({
1218
1509
  logger.error(`Service-account auth failed: ${e.message}`);
1219
1510
  process.exit(1);
1220
1511
  });
1512
+ const config = await loadConfig();
1513
+ config.serviceAccountPath = saPath;
1514
+ await saveConfig(config);
1221
1515
  logger.success(`Service-account verified: ${jwt.email ?? "OK"}`);
1222
- logger.info(`Set GOOGLE_APPLICATION_CREDENTIALS=${args["service-account"]} to use it across sessions.`);
1516
+ logger.info(`Saved path to config: ${saPath}`);
1223
1517
  return;
1224
1518
  }
1225
1519
  if (args.force) await clearTokens();
@@ -1232,6 +1526,14 @@ const authCommand = defineCommand({
1232
1526
  process.exit(1);
1233
1527
  });
1234
1528
  logger.success("Logged in");
1529
+ if (!resolveActiveProfile()) {
1530
+ const tokens = await loadTokens();
1531
+ const info = tokens?.access_token ? await fetchTokenInfo(tokens.access_token) : null;
1532
+ if (info?.email) {
1533
+ const name = profileNameFromEmail(info.email);
1534
+ if (await adoptCurrentConfigAsProfile(name).catch(() => null)) logger.success(`Saved as profile "${name}" (active)`);
1535
+ }
1536
+ }
1235
1537
  if (resolveBYOK()) {
1236
1538
  console.log();
1237
1539
  console.log(await formatAuthProvenance());
@@ -1243,18 +1545,44 @@ const authCommand = defineCommand({
1243
1545
  name: "logout",
1244
1546
  description: "Clear stored OAuth tokens"
1245
1547
  },
1246
- args: { quiet: {
1247
- type: "boolean",
1248
- alias: "q",
1249
- default: false,
1250
- description: "Suppress info/success output"
1251
- } },
1548
+ args: { ...OUTPUT_ARGS },
1252
1549
  async run({ args }) {
1253
- setQuiet(Boolean(args.quiet));
1550
+ applyOutputMode(args);
1254
1551
  await clearTokens();
1552
+ const config = await loadConfig();
1553
+ if (config.serviceAccountPath) {
1554
+ delete config.serviceAccountPath;
1555
+ await saveConfig(config);
1556
+ logger.info("Cleared saved service-account path");
1557
+ }
1255
1558
  }
1256
1559
  }),
1257
- refresh: refreshCommand
1560
+ refresh: refreshCommand,
1561
+ scopes: defineCommand({
1562
+ meta: {
1563
+ name: "scopes",
1564
+ description: "Print granted OAuth scopes (one per line); exits 1 if any required scope is missing"
1565
+ },
1566
+ args: { ...OUTPUT_ARGS },
1567
+ async run({ args }) {
1568
+ const { json } = applyOutputMode(args);
1569
+ const { liveToken, scopes, missing } = await resolveLiveAuthState();
1570
+ if (!liveToken) {
1571
+ if (json) console.log(JSON.stringify({
1572
+ scopes: [],
1573
+ missing: null
1574
+ }, null, 2));
1575
+ else logger.error("Not authenticated");
1576
+ process.exit(1);
1577
+ }
1578
+ if (json) console.log(JSON.stringify({
1579
+ scopes,
1580
+ missing
1581
+ }, null, 2));
1582
+ else for (const s of scopes) console.log(s);
1583
+ if (missing.length > 0) process.exit(1);
1584
+ }
1585
+ })
1258
1586
  }
1259
1587
  });
1260
1588
  const showCommand = defineCommand({
@@ -1262,24 +1590,12 @@ const showCommand = defineCommand({
1262
1590
  name: "show",
1263
1591
  description: "Show current config"
1264
1592
  },
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
- },
1593
+ args: { ...OUTPUT_ARGS },
1278
1594
  async run({ args }) {
1279
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
1595
+ const { json } = applyOutputMode(args);
1280
1596
  const config = await loadConfig();
1281
1597
  const configPath = getConfigPath();
1282
- if (args.json) {
1598
+ if (json) {
1283
1599
  console.log(JSON.stringify({
1284
1600
  path: configPath,
1285
1601
  config
@@ -1303,7 +1619,8 @@ const VALID_KEYS = [
1303
1619
  "dataDir",
1304
1620
  "defaultLimit",
1305
1621
  "defaultSearchType",
1306
- "defaultDataState"
1622
+ "defaultDataState",
1623
+ "serviceAccountPath"
1307
1624
  ];
1308
1625
  const NUMERIC_KEYS = new Set(["defaultLimit"]);
1309
1626
  const configCommand = defineCommand({
@@ -1329,15 +1646,10 @@ const configCommand = defineCommand({
1329
1646
  description: "Value to set",
1330
1647
  required: true
1331
1648
  },
1332
- quiet: {
1333
- type: "boolean",
1334
- alias: "q",
1335
- default: false,
1336
- description: "Suppress info/success output"
1337
- }
1649
+ ...OUTPUT_ARGS
1338
1650
  },
1339
1651
  async run({ args }) {
1340
- setQuiet(Boolean(args.quiet));
1652
+ applyOutputMode(args);
1341
1653
  if (!VALID_KEYS.includes(args.key)) {
1342
1654
  logger.error(`Invalid key: ${args.key}`);
1343
1655
  logger.info(`Valid keys: ${VALID_KEYS.join(", ")}`);
@@ -1365,15 +1677,10 @@ const configCommand = defineCommand({
1365
1677
  description: `Config key to remove (${VALID_KEYS.join(", ")})`,
1366
1678
  required: true
1367
1679
  },
1368
- quiet: {
1369
- type: "boolean",
1370
- alias: "q",
1371
- default: false,
1372
- description: "Suppress info/success output"
1373
- }
1680
+ ...OUTPUT_ARGS
1374
1681
  },
1375
1682
  async run({ args }) {
1376
- setQuiet(Boolean(args.quiet));
1683
+ applyOutputMode(args);
1377
1684
  if (!VALID_KEYS.includes(args.key)) {
1378
1685
  logger.error(`Invalid key: ${args.key}`);
1379
1686
  logger.info(`Valid keys: ${VALID_KEYS.join(", ")}`);
@@ -1399,21 +1706,9 @@ const configCommand = defineCommand({
1399
1706
  name: "validate",
1400
1707
  description: "Validate the saved config (defaultSite is verified, dataDir exists/writable)"
1401
1708
  },
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
- },
1709
+ args: { ...OUTPUT_ARGS },
1415
1710
  async run({ args }) {
1416
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
1711
+ const { json } = applyOutputMode(args);
1417
1712
  const { resolveDataDir } = await import("./_chunks/config.mjs").then((n) => n.t);
1418
1713
  const fs = await import("node:fs/promises");
1419
1714
  const config = await loadConfig();
@@ -1459,7 +1754,31 @@ const configCommand = defineCommand({
1459
1754
  level: "fail",
1460
1755
  message: `unknown format: ${config.defaultFormat}`
1461
1756
  });
1462
- if (args.json) {
1757
+ const { SearchTypes } = await import("gscdump/query");
1758
+ const allowedSearchTypes = Object.values(SearchTypes);
1759
+ if (config.defaultSearchType && !allowedSearchTypes.includes(config.defaultSearchType)) issues.push({
1760
+ key: "defaultSearchType",
1761
+ level: "fail",
1762
+ message: `unknown search type: ${config.defaultSearchType} (allowed: ${allowedSearchTypes.join(", ")})`
1763
+ });
1764
+ const allowedDataStates = [
1765
+ "all",
1766
+ "final",
1767
+ "hourly_all"
1768
+ ];
1769
+ if (config.defaultDataState && !allowedDataStates.includes(config.defaultDataState)) issues.push({
1770
+ key: "defaultDataState",
1771
+ level: "fail",
1772
+ message: `unknown data state: ${config.defaultDataState} (allowed: ${allowedDataStates.join(", ")})`
1773
+ });
1774
+ if (config.serviceAccountPath) {
1775
+ if (!await fs.stat(config.serviceAccountPath).catch(() => null)) issues.push({
1776
+ key: "serviceAccountPath",
1777
+ level: "fail",
1778
+ message: `${displayPath(config.serviceAccountPath)} does not exist`
1779
+ });
1780
+ }
1781
+ if (json) {
1463
1782
  console.log(JSON.stringify({
1464
1783
  ok: !issues.some((i) => i.level === "fail"),
1465
1784
  issues
@@ -1798,12 +2117,9 @@ const doctorCommand = defineCommand({
1798
2117
  name: "doctor",
1799
2118
  description: "Run health checks (env, auth, scopes, time, dataDir, store, GSC reachability + ping, defaultSite)"
1800
2119
  },
1801
- args: { json: {
1802
- type: "boolean",
1803
- default: false,
1804
- description: "Output as JSON"
1805
- } },
2120
+ args: { ...OUTPUT_ARGS },
1806
2121
  async run({ args }) {
2122
+ const { json } = applyOutputMode(args);
1807
2123
  const envResult = await checkEnv();
1808
2124
  const [authResult, timeChecks, dataDirChecks, watermarkChecks, gscApi, indexingApi, siteVerificationApi] = await Promise.all([
1809
2125
  checkAuth$1(envResult.envKeys),
@@ -1830,7 +2146,7 @@ const doctorCommand = defineCommand({
1830
2146
  ...siteVerificationApi,
1831
2147
  ...sitesChecks
1832
2148
  ];
1833
- if (args.json) {
2149
+ if (json) {
1834
2150
  console.log(JSON.stringify({
1835
2151
  checks: all,
1836
2152
  ok: all.every((c) => c.status !== "fail")
@@ -1904,20 +2220,10 @@ const dumpCommand = defineCommand({
1904
2220
  default: false,
1905
2221
  description: "Compact every closed month into a single file before exporting"
1906
2222
  },
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
- }
2223
+ ...OUTPUT_ARGS
1918
2224
  },
1919
2225
  async run({ args }) {
1920
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
2226
+ const { json, quiet } = applyOutputMode(args);
1921
2227
  const format = String(args.format);
1922
2228
  if (!FORMATS.includes(format)) {
1923
2229
  logger.error(`Invalid --format: ${format}. Allowed: ${FORMATS.join(", ")}`);
@@ -1935,12 +2241,12 @@ const dumpCommand = defineCommand({
1935
2241
  logger.warn("No sites with local data. Run `gscdump sync` first.");
1936
2242
  process.exit(0);
1937
2243
  }
1938
- if (args.compact) for (const siteUrl of targets) await compactClosedMonths(store, siteUrl, args.quiet);
2244
+ if (args.compact) for (const siteUrl of targets) await compactClosedMonths(store, siteUrl, quiet);
1939
2245
  const summary = [];
1940
2246
  for (const siteUrl of targets) {
1941
2247
  const entries = (await listLiveEntries(store, siteUrl)).filter((e) => !tablesFilter || tablesFilter.has(e.table));
1942
2248
  if (entries.length === 0) {
1943
- if (!args.json && !args.quiet) logger.warn(`No data for ${siteUrl}; skipping`);
2249
+ if (!quiet) logger.warn(`No data for ${siteUrl}; skipping`);
1944
2250
  continue;
1945
2251
  }
1946
2252
  if (format === "parquet") {
@@ -1963,7 +2269,7 @@ const dumpCommand = defineCommand({
1963
2269
  });
1964
2270
  }
1965
2271
  }
1966
- if (args.json) {
2272
+ if (json) {
1967
2273
  console.log(JSON.stringify({
1968
2274
  outDir,
1969
2275
  sites: summary
@@ -2092,20 +2398,10 @@ const inspectSubCommand = defineCommand({
2092
2398
  default: "4",
2093
2399
  description: "Concurrent in-flight inspect calls (default: 4)"
2094
2400
  },
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
- }
2401
+ ...OUTPUT_ARGS
2106
2402
  },
2107
2403
  async run({ args }) {
2108
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
2404
+ const { json, quiet } = applyOutputMode(args);
2109
2405
  const ctx = await createCommandContext({
2110
2406
  needsAuth: true,
2111
2407
  needsStore: true
@@ -2115,7 +2411,6 @@ const inspectSubCommand = defineCommand({
2115
2411
  const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
2116
2412
  const limit = args.limit ? Number.parseInt(String(args.limit), 10) : INSPECTION_QPD_PER_PROPERTY;
2117
2413
  const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 4);
2118
- const quiet = Boolean(args.quiet) || Boolean(args.json);
2119
2414
  const urls = (await readUrlList({ file: args.file ? String(args.file) : void 0 })).slice(0, limit);
2120
2415
  if (urls.length === 0) {
2121
2416
  logger.warn("No URLs to inspect.");
@@ -2163,7 +2458,7 @@ const inspectSubCommand = defineCommand({
2163
2458
  userId: store.userId,
2164
2459
  siteId: store.siteIdFor(siteUrl)
2165
2460
  }, records);
2166
- if (args.json) console.log(JSON.stringify({
2461
+ if (json) console.log(JSON.stringify({
2167
2462
  site: siteUrl,
2168
2463
  inspected: records.length,
2169
2464
  failed,
@@ -2187,6 +2482,7 @@ const showSubCommand = defineCommand({
2187
2482
  description: "Print the latest inspection record for a URL from the local entity store"
2188
2483
  },
2189
2484
  args: {
2485
+ ...OUTPUT_ARGS,
2190
2486
  site: {
2191
2487
  type: "string",
2192
2488
  alias: "s",
@@ -2196,14 +2492,10 @@ const showSubCommand = defineCommand({
2196
2492
  type: "positional",
2197
2493
  required: true,
2198
2494
  description: "URL to look up"
2199
- },
2200
- json: {
2201
- type: "boolean",
2202
- default: false,
2203
- description: "Output as JSON"
2204
2495
  }
2205
2496
  },
2206
2497
  async run({ args }) {
2498
+ const { json } = applyOutputMode(args);
2207
2499
  const ctx = await createCommandContext({
2208
2500
  needsAuth: true,
2209
2501
  needsStore: true
@@ -2218,7 +2510,7 @@ const showSubCommand = defineCommand({
2218
2510
  logger.warn(`No inspection record for ${args.url}`);
2219
2511
  process.exit(1);
2220
2512
  }
2221
- if (args.json) {
2513
+ if (json) {
2222
2514
  console.log(JSON.stringify(record, null, 2));
2223
2515
  return;
2224
2516
  }
@@ -2240,24 +2532,15 @@ const sitemapsSnapshotSubCommand = defineCommand({
2240
2532
  description: "Fetch current sitemap state from GSC and persist to the local entity store"
2241
2533
  },
2242
2534
  args: {
2535
+ ...OUTPUT_ARGS,
2243
2536
  site: {
2244
2537
  type: "string",
2245
2538
  alias: "s",
2246
2539
  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
2540
  }
2259
2541
  },
2260
2542
  async run({ args }) {
2543
+ const { json, quiet } = applyOutputMode(args);
2261
2544
  const ctx = await createCommandContext({
2262
2545
  needsAuth: true,
2263
2546
  needsStore: true
@@ -2265,7 +2548,6 @@ const sitemapsSnapshotSubCommand = defineCommand({
2265
2548
  const client = ctx.client;
2266
2549
  const store = ctx.store;
2267
2550
  const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
2268
- const quiet = Boolean(args.quiet);
2269
2551
  const apiSitemaps = await client.sitemaps.list(siteUrl);
2270
2552
  const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
2271
2553
  const records = apiSitemaps.filter((s) => typeof s.path === "string").map((s) => ({
@@ -2289,7 +2571,7 @@ const sitemapsSnapshotSubCommand = defineCommand({
2289
2571
  userId: store.userId,
2290
2572
  siteId: store.siteIdFor(siteUrl)
2291
2573
  }, records);
2292
- if (args.json) {
2574
+ if (json) {
2293
2575
  console.log(JSON.stringify({
2294
2576
  site: siteUrl,
2295
2577
  capturedAt,
@@ -2314,6 +2596,7 @@ const sitemapsShowSubCommand = defineCommand({
2314
2596
  description: "Print the latest captured sitemap state for a feedpath"
2315
2597
  },
2316
2598
  args: {
2599
+ ...OUTPUT_ARGS,
2317
2600
  site: {
2318
2601
  type: "string",
2319
2602
  alias: "s",
@@ -2323,14 +2606,10 @@ const sitemapsShowSubCommand = defineCommand({
2323
2606
  type: "positional",
2324
2607
  required: true,
2325
2608
  description: "Sitemap path (feedpath)"
2326
- },
2327
- json: {
2328
- type: "boolean",
2329
- default: false,
2330
- description: "Output as JSON"
2331
2609
  }
2332
2610
  },
2333
2611
  async run({ args }) {
2612
+ const { json } = applyOutputMode(args);
2334
2613
  const ctx = await createCommandContext({
2335
2614
  needsAuth: true,
2336
2615
  needsStore: true
@@ -2345,7 +2624,7 @@ const sitemapsShowSubCommand = defineCommand({
2345
2624
  logger.warn(`No sitemap record for ${args.path}`);
2346
2625
  process.exit(1);
2347
2626
  }
2348
- if (args.json) {
2627
+ if (json) {
2349
2628
  console.log(JSON.stringify(record, null, 2));
2350
2629
  return;
2351
2630
  }
@@ -2398,14 +2677,10 @@ const indexingSubCommand = defineCommand({
2398
2677
  default: "4",
2399
2678
  description: "Concurrent in-flight getMetadata calls (default: 4)"
2400
2679
  },
2401
- quiet: {
2402
- type: "boolean",
2403
- alias: "q",
2404
- default: false,
2405
- description: "Suppress progress output"
2406
- }
2680
+ ...OUTPUT_ARGS
2407
2681
  },
2408
2682
  async run({ args }) {
2683
+ const { quiet } = applyOutputMode(args);
2409
2684
  const ctx = await createCommandContext({
2410
2685
  needsAuth: true,
2411
2686
  needsStore: true
@@ -2414,7 +2689,6 @@ const indexingSubCommand = defineCommand({
2414
2689
  const store = ctx.store;
2415
2690
  const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
2416
2691
  const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 4);
2417
- const quiet = Boolean(args.quiet);
2418
2692
  const urls = await readUrlList({ file: args.file ? String(args.file) : void 0 });
2419
2693
  if (urls.length === 0) {
2420
2694
  logger.warn("No URLs to fetch metadata for.");
@@ -2490,6 +2764,14 @@ function parseRetries(v) {
2490
2764
  const n = Number.parseInt(String(v), 10);
2491
2765
  return Number.isFinite(n) && n >= 0 ? n : void 0;
2492
2766
  }
2767
+ async function resolveUrlSource(args) {
2768
+ const fromSitemap = args["from-sitemap"];
2769
+ if (fromSitemap) return fetchSitemapUrls(String(fromSitemap)).catch((e) => {
2770
+ logger.error(`Sitemap fetch failed: ${e.message}`);
2771
+ process.exit(1);
2772
+ });
2773
+ return readUrlList$1(args);
2774
+ }
2493
2775
  const submitCommand$1 = defineCommand({
2494
2776
  meta: {
2495
2777
  name: "submit",
@@ -2501,21 +2783,11 @@ const submitCommand$1 = defineCommand({
2501
2783
  required: true,
2502
2784
  description: "URL to submit"
2503
2785
  },
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
- },
2786
+ ...OUTPUT_ARGS,
2515
2787
  ...RETRIES_ARG
2516
2788
  },
2517
2789
  async run({ args }) {
2518
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
2790
+ applyOutputMode(args);
2519
2791
  const result = await requestIndexing((await createCommandContext({
2520
2792
  needsAuth: true,
2521
2793
  fetchOptions: { retry: parseRetries(args.retries) }
@@ -2539,21 +2811,11 @@ const removeCommand = defineCommand({
2539
2811
  required: true,
2540
2812
  description: "URL to mark removed"
2541
2813
  },
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
- },
2814
+ ...OUTPUT_ARGS,
2553
2815
  ...RETRIES_ARG
2554
2816
  },
2555
2817
  async run({ args }) {
2556
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
2818
+ applyOutputMode(args);
2557
2819
  const result = await requestIndexing((await createCommandContext({
2558
2820
  needsAuth: true,
2559
2821
  fetchOptions: { retry: parseRetries(args.retries) }
@@ -2590,7 +2852,7 @@ const statusCommand = defineCommand({
2590
2852
  }
2591
2853
  },
2592
2854
  async run({ args }) {
2593
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
2855
+ applyOutputMode(args);
2594
2856
  const meta = await getIndexingMetadata((await createCommandContext({ needsAuth: true })).client, args.url).catch(gscErrorHandler);
2595
2857
  if (args.json) {
2596
2858
  console.log(JSON.stringify(meta, null, 2));
@@ -2610,31 +2872,65 @@ const statusCommand = defineCommand({
2610
2872
  }
2611
2873
  });
2612
2874
  const INDEXING_DAILY_QUOTA = 200;
2875
+ const INDEXING_PER_MINUTE_QUOTA = 600;
2876
+ const quotaCommand = defineCommand({
2877
+ meta: {
2878
+ name: "quota",
2879
+ description: "Show documented Indexing API quotas (no live counters; quota usage is not exposed by the API)"
2880
+ },
2881
+ args: { ...OUTPUT_ARGS },
2882
+ async run({ args }) {
2883
+ const { json } = applyOutputMode(args);
2884
+ const payload = {
2885
+ perDay: INDEXING_DAILY_QUOTA,
2886
+ perMinute: INDEXING_PER_MINUTE_QUOTA,
2887
+ note: "Documented defaults. Google does not expose live counters; track yours by counting submit calls.",
2888
+ docs: "https://developers.google.com/search/apis/indexing-api/v3/quota-pricing"
2889
+ };
2890
+ if (json) {
2891
+ console.log(JSON.stringify(payload, null, 2));
2892
+ return;
2893
+ }
2894
+ console.log();
2895
+ console.log(` \x1B[1mIndexing API quota\x1B[0m`);
2896
+ console.log(` Per day: ${payload.perDay}`);
2897
+ console.log(` Per minute: ${payload.perMinute}`);
2898
+ console.log();
2899
+ console.log(` \x1B[90m${payload.note}\x1B[0m`);
2900
+ console.log(` \x1B[90mDocs: ${payload.docs}\x1B[0m`);
2901
+ console.log();
2902
+ }
2903
+ });
2613
2904
  const indexingCommand = defineCommand({
2614
2905
  meta: {
2615
2906
  name: "indexing",
2616
2907
  description: "Notify Google about URL updates/removals (Indexing API)"
2617
2908
  },
2618
2909
  subCommands: {
2619
- submit: submitCommand$1,
2620
- remove: removeCommand,
2621
- status: statusCommand,
2622
- batch: defineCommand({
2910
+ "submit": submitCommand$1,
2911
+ "remove": removeCommand,
2912
+ "status": statusCommand,
2913
+ "batch": defineCommand({
2623
2914
  meta: {
2624
2915
  name: "batch",
2625
2916
  description: "Submit many URLs from a file or stdin (one URL per line)"
2626
2917
  },
2627
2918
  args: {
2919
+ ...OUTPUT_ARGS,
2628
2920
  "urls": {
2629
2921
  type: "positional",
2630
2922
  required: false,
2631
- description: "URLs (or use --file/stdin)"
2923
+ description: "URLs (or use --file/--from-sitemap/stdin)"
2632
2924
  },
2633
2925
  "file": {
2634
2926
  type: "string",
2635
2927
  alias: "f",
2636
2928
  description: "File with URLs (one per line)"
2637
2929
  },
2930
+ "from-sitemap": {
2931
+ type: "string",
2932
+ description: "Sitemap URL (or sitemap index) to pull URLs from"
2933
+ },
2638
2934
  "type": {
2639
2935
  type: "string",
2640
2936
  default: "URL_UPDATED",
@@ -2651,17 +2947,6 @@ const indexingCommand = defineCommand({
2651
2947
  default: "1",
2652
2948
  description: "Concurrent in-flight requests"
2653
2949
  },
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
2950
  "yes": {
2666
2951
  type: "boolean",
2667
2952
  alias: "y",
@@ -2674,10 +2959,10 @@ const indexingCommand = defineCommand({
2674
2959
  }
2675
2960
  },
2676
2961
  async run({ args }) {
2677
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
2678
- const urls = await readUrlList$1(args);
2962
+ applyOutputMode(args);
2963
+ const urls = await resolveUrlSource(args);
2679
2964
  if (urls.length === 0) {
2680
- logger.error("No URLs provided. Pass URLs as args, --file, or stdin.");
2965
+ logger.error("No URLs provided. Pass URLs as args, --file, --from-sitemap, or stdin.");
2681
2966
  process.exit(1);
2682
2967
  }
2683
2968
  const type = String(args.type);
@@ -2710,7 +2995,71 @@ const indexingCommand = defineCommand({
2710
2995
  }
2711
2996
  if (!args.quiet) logger.success(`Submitted ${results.length}/${urls.length} URLs`);
2712
2997
  }
2713
- })
2998
+ }),
2999
+ "batch-status": defineCommand({
3000
+ meta: {
3001
+ name: "batch-status",
3002
+ description: "Get indexing notification metadata for many URLs"
3003
+ },
3004
+ args: {
3005
+ ...OUTPUT_ARGS,
3006
+ "urls": {
3007
+ type: "positional",
3008
+ required: false,
3009
+ description: "URLs (or use --file/--from-sitemap/stdin)"
3010
+ },
3011
+ "file": {
3012
+ type: "string",
3013
+ alias: "f",
3014
+ description: "File with URLs (one per line)"
3015
+ },
3016
+ "from-sitemap": {
3017
+ type: "string",
3018
+ description: "Sitemap URL (or sitemap index) to pull URLs from"
3019
+ },
3020
+ "delay-ms": {
3021
+ type: "string",
3022
+ default: "100",
3023
+ description: "Delay between requests"
3024
+ },
3025
+ "concurrency": {
3026
+ type: "string",
3027
+ alias: "c",
3028
+ default: "1",
3029
+ description: "Concurrent in-flight requests"
3030
+ },
3031
+ "retries": {
3032
+ type: "string",
3033
+ description: "Override per-call retry count (default: 3)"
3034
+ }
3035
+ },
3036
+ async run({ args }) {
3037
+ applyOutputMode(args);
3038
+ const urls = await resolveUrlSource(args);
3039
+ if (urls.length === 0) {
3040
+ logger.error("No URLs provided. Pass URLs as args, --file, --from-sitemap, or stdin.");
3041
+ process.exit(1);
3042
+ }
3043
+ const ctx = await createCommandContext({
3044
+ needsAuth: true,
3045
+ fetchOptions: { retry: parseRetries(args.retries) }
3046
+ });
3047
+ const delayMs = Number.parseInt(String(args["delay-ms"]), 10);
3048
+ const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 1);
3049
+ if (!args.json && !args.quiet) logger.info(`Fetching status for ${urls.length} URLs ...`);
3050
+ const results = await runSequentialBatch(urls, (url) => getIndexingMetadata(ctx.client, url), {
3051
+ delayMs,
3052
+ concurrency,
3053
+ onProgress: args.json || args.quiet ? void 0 : (r, i, total) => logger.info(`[${i + 1}/${total}] ${r.url}`)
3054
+ }).catch(gscErrorHandler);
3055
+ if (args.json) {
3056
+ console.log(JSON.stringify(results, null, 2));
3057
+ return;
3058
+ }
3059
+ if (!args.quiet) logger.success(`Fetched ${results.length}/${urls.length} URLs`);
3060
+ }
3061
+ }),
3062
+ "quota": quotaCommand
2714
3063
  }
2715
3064
  });
2716
3065
  const ENV_LINE_RE = /^([^=]+)=(.*)$/;
@@ -2758,15 +3107,10 @@ const initCommand = defineCommand({
2758
3107
  default: false,
2759
3108
  description: "Skip dataDir prompt (auth-only setup)"
2760
3109
  },
2761
- "quiet": {
2762
- type: "boolean",
2763
- alias: "q",
2764
- default: false,
2765
- description: "Suppress info/success output"
2766
- }
3110
+ ...OUTPUT_ARGS
2767
3111
  },
2768
3112
  async run({ args }) {
2769
- setQuiet(Boolean(args.quiet));
3113
+ applyOutputMode(args);
2770
3114
  const config = await loadConfig();
2771
3115
  if (config.clientId && config.clientSecret && !args.force) {
2772
3116
  logger.info("Already configured");
@@ -2946,12 +3290,91 @@ function printInspection(url, inspection) {
2946
3290
  }
2947
3291
  console.log();
2948
3292
  }
3293
+ const batchCommand = defineCommand({
3294
+ meta: {
3295
+ name: "batch",
3296
+ description: "Inspect many URLs from a file or stdin (one URL per line)"
3297
+ },
3298
+ args: {
3299
+ ...OUTPUT_ARGS,
3300
+ "site": {
3301
+ type: "string",
3302
+ alias: "s",
3303
+ description: "Site URL (defaults to config.defaultSite or prompt)"
3304
+ },
3305
+ "urls": {
3306
+ type: "positional",
3307
+ required: false,
3308
+ description: "URLs (or use --file/--from-sitemap/stdin)"
3309
+ },
3310
+ "file": {
3311
+ type: "string",
3312
+ alias: "f",
3313
+ description: "File with URLs (one per line)"
3314
+ },
3315
+ "from-sitemap": {
3316
+ type: "string",
3317
+ description: "Sitemap URL (or sitemap index) to pull URLs from"
3318
+ },
3319
+ "delay-ms": {
3320
+ type: "string",
3321
+ default: "200",
3322
+ description: "Delay between requests"
3323
+ },
3324
+ "concurrency": {
3325
+ type: "string",
3326
+ alias: "c",
3327
+ default: "1",
3328
+ description: "Concurrent in-flight requests"
3329
+ }
3330
+ },
3331
+ async run({ args }) {
3332
+ const { json, quiet } = applyOutputMode(args);
3333
+ const urls = args["from-sitemap"] ? await fetchSitemapUrls(String(args["from-sitemap"])).catch((e) => {
3334
+ logger.error(`Sitemap fetch failed: ${e.message}`);
3335
+ process.exit(1);
3336
+ }) : await readUrlList$1(args);
3337
+ if (urls.length === 0) {
3338
+ logger.error("No URLs provided. Pass URLs as args, --file, --from-sitemap, or stdin.");
3339
+ process.exit(1);
3340
+ }
3341
+ const ctx = await createCommandContext({ needsAuth: true });
3342
+ const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
3343
+ const delayMs = Number.parseInt(String(args["delay-ms"]), 10);
3344
+ const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 1);
3345
+ if (!quiet) logger.info(`Inspecting ${urls.length} URLs ...`);
3346
+ const results = await batchInspectUrls(ctx.client, siteUrl, urls, {
3347
+ delayMs,
3348
+ concurrency,
3349
+ onProgress: quiet ? void 0 : (r, i, total) => logger.info(`[${i + 1}/${total}] ${r.url} ${r.isIndexed ? "PASS" : "FAIL"}`)
3350
+ }).catch(gscErrorHandler);
3351
+ if (json) {
3352
+ const flattened = results.map((r) => {
3353
+ const indexStatus = r.inspection?.indexStatusResult;
3354
+ return {
3355
+ url: r.url,
3356
+ verdict: indexStatus?.verdict || null,
3357
+ coverageState: indexStatus?.coverageState || null,
3358
+ indexingState: indexStatus?.indexingState || null,
3359
+ lastCrawlTime: indexStatus?.lastCrawlTime || null,
3360
+ isIndexed: r.isIndexed,
3361
+ raw: r.inspection
3362
+ };
3363
+ });
3364
+ console.log(JSON.stringify(flattened, null, 2));
3365
+ return;
3366
+ }
3367
+ const indexed = results.filter((r) => r.isIndexed).length;
3368
+ if (!quiet) logger.success(`Inspected ${results.length} URLs (${indexed} indexed, ${results.length - indexed} not)`);
3369
+ }
3370
+ });
2949
3371
  const inspectCommand = defineCommand({
2950
3372
  meta: {
2951
3373
  name: "inspect",
2952
3374
  description: "Inspect URL indexing status (single URL; use `inspect batch` for many)"
2953
3375
  },
2954
3376
  args: {
3377
+ ...OUTPUT_ARGS,
2955
3378
  site: {
2956
3379
  type: "string",
2957
3380
  alias: "s",
@@ -2961,96 +3384,17 @@ const inspectCommand = defineCommand({
2961
3384
  type: "positional",
2962
3385
  required: true,
2963
3386
  description: "URL to inspect"
2964
- },
2965
- json: {
2966
- type: "boolean",
2967
- default: false,
2968
- description: "Output as JSON"
2969
- },
2970
- quiet: {
2971
- type: "boolean",
2972
- alias: "q",
2973
- default: false,
2974
- description: "Suppress info/success output"
2975
3387
  }
2976
3388
  },
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
- }
3020
- },
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)`);
3044
- }
3045
- }) },
3389
+ subCommands: { batch: batchCommand },
3046
3390
  async run({ args }) {
3047
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
3391
+ const { json } = applyOutputMode(args);
3048
3392
  const ctx = await createCommandContext({ needsAuth: true });
3049
3393
  const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
3050
3394
  const result = await ctx.client.inspect(siteUrl, args.url).catch(gscErrorHandler);
3051
3395
  const inspection = result?.inspectionResult;
3052
3396
  const indexStatus = inspection?.indexStatusResult;
3053
- if (args.json) {
3397
+ if (json) {
3054
3398
  console.log(JSON.stringify({
3055
3399
  url: args.url,
3056
3400
  verdict: indexStatus?.verdict || null,
@@ -3634,16 +3978,12 @@ const sitemapsCommand = defineCommand({
3634
3978
  description: "List sitemaps for a site"
3635
3979
  },
3636
3980
  args: {
3981
+ ...OUTPUT_ARGS,
3637
3982
  site: {
3638
3983
  type: "string",
3639
3984
  alias: "s",
3640
3985
  description: "Site URL (e.g., sc-domain:example.com or https://example.com/)"
3641
3986
  },
3642
- json: {
3643
- type: "boolean",
3644
- default: false,
3645
- description: "Output as JSON"
3646
- },
3647
3987
  pending: {
3648
3988
  type: "boolean",
3649
3989
  default: false,
@@ -3656,12 +3996,10 @@ const sitemapsCommand = defineCommand({
3656
3996
  }
3657
3997
  },
3658
3998
  async run({ args }) {
3999
+ const { json } = applyOutputMode(args);
3659
4000
  const ctx = await createCommandContext({ needsAuth: true });
3660
4001
  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) => ({
4002
+ let sitemaps = (await ctx.client.sitemaps.list(siteUrl).catch(gscErrorHandler)).map((sm) => ({
3665
4003
  path: sm.path,
3666
4004
  type: sm.type || void 0,
3667
4005
  isPending: sm.isPending || false,
@@ -3672,7 +4010,7 @@ const sitemapsCommand = defineCommand({
3672
4010
  }));
3673
4011
  if (args.pending) sitemaps = sitemaps.filter((sm) => sm.isPending);
3674
4012
  if (args.errored) sitemaps = sitemaps.filter((sm) => sm.errors > 0);
3675
- if (args.json) {
4013
+ if (json) {
3676
4014
  console.log(JSON.stringify(sitemaps, null, 2));
3677
4015
  return;
3678
4016
  }
@@ -3697,6 +4035,7 @@ const sitemapsCommand = defineCommand({
3697
4035
  description: "Get details for a specific sitemap"
3698
4036
  },
3699
4037
  args: {
4038
+ ...OUTPUT_ARGS,
3700
4039
  site: {
3701
4040
  type: "string",
3702
4041
  alias: "s",
@@ -3706,19 +4045,15 @@ const sitemapsCommand = defineCommand({
3706
4045
  type: "positional",
3707
4046
  required: true,
3708
4047
  description: "Sitemap URL"
3709
- },
3710
- json: {
3711
- type: "boolean",
3712
- default: false,
3713
- description: "Output as JSON"
3714
4048
  }
3715
4049
  },
3716
4050
  async run({ args }) {
4051
+ const { json } = applyOutputMode(args);
3717
4052
  const ctx = await createCommandContext({ needsAuth: true });
3718
4053
  const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
3719
4054
  const client = ctx.client;
3720
4055
  const sitemap = await fetchSitemap(client, siteUrl, args.url).catch(gscErrorHandler);
3721
- if (args.json) {
4056
+ if (json) {
3722
4057
  console.log(JSON.stringify(sitemap, null, 2));
3723
4058
  return;
3724
4059
  }
@@ -3743,6 +4078,7 @@ const sitemapsCommand = defineCommand({
3743
4078
  description: "Submit a sitemap to GSC"
3744
4079
  },
3745
4080
  args: {
4081
+ ...OUTPUT_ARGS,
3746
4082
  site: {
3747
4083
  type: "string",
3748
4084
  alias: "s",
@@ -3755,12 +4091,18 @@ const sitemapsCommand = defineCommand({
3755
4091
  }
3756
4092
  },
3757
4093
  async run({ args }) {
4094
+ const { json } = applyOutputMode(args);
3758
4095
  const ctx = await createCommandContext({ needsAuth: true });
3759
4096
  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
- });
4097
+ await ctx.client.sitemaps.submit(siteUrl, args.url).catch(gscErrorHandler);
4098
+ if (json) {
4099
+ console.log(JSON.stringify({
4100
+ siteUrl,
4101
+ feedpath: args.url,
4102
+ status: "submitted"
4103
+ }, null, 2));
4104
+ return;
4105
+ }
3764
4106
  logger.success(`Submitted sitemap: ${args.url}`);
3765
4107
  }
3766
4108
  }),
@@ -3770,6 +4112,7 @@ const sitemapsCommand = defineCommand({
3770
4112
  description: "Delete a sitemap from GSC"
3771
4113
  },
3772
4114
  args: {
4115
+ ...OUTPUT_ARGS,
3773
4116
  site: {
3774
4117
  type: "string",
3775
4118
  alias: "s",
@@ -3782,26 +4125,107 @@ const sitemapsCommand = defineCommand({
3782
4125
  }
3783
4126
  },
3784
4127
  async run({ args }) {
4128
+ const { json } = applyOutputMode(args);
3785
4129
  const ctx = await createCommandContext({ needsAuth: true });
3786
4130
  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}`);
3789
- process.exit(1);
3790
- });
4131
+ await ctx.client.sitemaps.delete(siteUrl, args.url).catch(gscErrorHandler);
4132
+ if (json) {
4133
+ console.log(JSON.stringify({
4134
+ siteUrl,
4135
+ feedpath: args.url,
4136
+ status: "deleted"
4137
+ }, null, 2));
4138
+ return;
4139
+ }
3791
4140
  logger.success(`Deleted sitemap: ${args.url}`);
3792
4141
  }
3793
- })
3794
- }
3795
- });
3796
- const ALL_METHODS = [
3797
- "META",
3798
- "FILE",
3799
- "DNS_TXT",
3800
- "DNS_CNAME",
3801
- "ANALYTICS",
3802
- "TAG_MANAGER"
3803
- ];
3804
- function pickDefaultMethod(siteUrl) {
4142
+ }),
4143
+ discover: defineCommand({
4144
+ meta: {
4145
+ name: "discover",
4146
+ description: "Probe a domain's robots.txt + common paths for an advertised sitemap (no auth needed)"
4147
+ },
4148
+ args: {
4149
+ ...OUTPUT_ARGS,
4150
+ domain: {
4151
+ type: "positional",
4152
+ required: true,
4153
+ description: "Domain (e.g., example.com)"
4154
+ }
4155
+ },
4156
+ async run({ args }) {
4157
+ const { json } = applyOutputMode(args);
4158
+ const domain = String(args.domain).replace(/^https?:\/\//, "").replace(/\/.*$/, "");
4159
+ const url = await discoverSitemap(domain).catch(() => null);
4160
+ if (json) {
4161
+ console.log(JSON.stringify({
4162
+ domain,
4163
+ sitemap: url
4164
+ }, null, 2));
4165
+ return;
4166
+ }
4167
+ if (!url) {
4168
+ logger.warn(`No sitemap discovered for ${domain}`);
4169
+ process.exit(1);
4170
+ }
4171
+ logger.success(`Discovered sitemap: ${url}`);
4172
+ }
4173
+ }),
4174
+ urls: defineCommand({
4175
+ meta: {
4176
+ name: "urls",
4177
+ description: "Fetch a sitemap (or sitemap index) and dump its <loc> URLs (no auth needed)"
4178
+ },
4179
+ args: {
4180
+ ...OUTPUT_ARGS,
4181
+ "url": {
4182
+ type: "positional",
4183
+ required: true,
4184
+ description: "Sitemap URL (index files are followed)"
4185
+ },
4186
+ "limit": {
4187
+ type: "string",
4188
+ alias: "l",
4189
+ description: "Stop after N URLs across all nested sitemaps"
4190
+ },
4191
+ "max-depth": {
4192
+ type: "string",
4193
+ description: "Max sitemap-index nesting depth (default: 3)"
4194
+ }
4195
+ },
4196
+ async run({ args }) {
4197
+ const { json } = applyOutputMode(args);
4198
+ const limit = args.limit ? Number.parseInt(String(args.limit), 10) : void 0;
4199
+ const maxDepth = args["max-depth"] ? Number.parseInt(String(args["max-depth"]), 10) : void 0;
4200
+ const urls = await fetchSitemapUrls(String(args.url), {
4201
+ limit,
4202
+ maxDepth
4203
+ }).catch((e) => {
4204
+ logger.error(`Sitemap fetch failed: ${e.message}`);
4205
+ process.exit(1);
4206
+ });
4207
+ if (json) {
4208
+ console.log(JSON.stringify({
4209
+ sitemap: args.url,
4210
+ count: urls.length,
4211
+ urls
4212
+ }, null, 2));
4213
+ return;
4214
+ }
4215
+ for (const u of urls) console.log(u);
4216
+ }
4217
+ })
4218
+ }
4219
+ });
4220
+ const ALL_METHODS = [
4221
+ "META",
4222
+ "FILE",
4223
+ "DNS_TXT",
4224
+ "DNS_CNAME",
4225
+ "ANALYTICS",
4226
+ "TAG_MANAGER"
4227
+ ];
4228
+ function pickDefaultMethod(siteUrl) {
3805
4229
  return siteUrl.startsWith("sc-domain:") ? "DNS_TXT" : "META";
3806
4230
  }
3807
4231
  function validateMethod(siteUrl, method) {
@@ -3847,281 +4271,442 @@ function printPlacementInstructions(method, siteUrl, token) {
3847
4271
  break;
3848
4272
  }
3849
4273
  case "ANALYTICS":
3850
- console.log(` Make sure your Google Analytics tracking tag is installed on the site, then run \`gscdump sites verify\`.`);
4274
+ console.log(` Make sure the Google Analytics tracking tag is installed on the site.`);
4275
+ console.log(` Expected tracking ID:`);
4276
+ console.log();
4277
+ console.log(` \x1B[2m${token}\x1B[0m`);
3851
4278
  break;
3852
4279
  case "TAG_MANAGER":
3853
- console.log(` Make sure your Google Tag Manager container snippet is installed on the site, then run \`gscdump sites verify\`.`);
4280
+ console.log(` Make sure the Google Tag Manager container snippet is installed on the site.`);
4281
+ console.log(` Expected container ID:`);
4282
+ console.log();
4283
+ console.log(` \x1B[2m${token}\x1B[0m`);
3854
4284
  break;
3855
4285
  }
3856
4286
  console.log();
3857
4287
  console.log(` \x1B[90mThen run:\x1B[0m gscdump sites verify ${siteUrl} --method ${method}`);
3858
4288
  console.log();
3859
4289
  }
3860
- const sitesCommand = defineCommand({
4290
+ const addCommand = defineCommand({
3861
4291
  meta: {
3862
- name: "sites",
3863
- description: "List GSC sites; manage properties (add/delete) and verify ownership"
4292
+ name: "add",
4293
+ description: "Register a property in Search Console (pass --verify to chain token + verify in one call)"
3864
4294
  },
3865
4295
  args: {
3866
- "json": {
3867
- type: "boolean",
3868
- default: false,
3869
- description: "Output as JSON for scripting"
4296
+ url: {
4297
+ type: "positional",
4298
+ required: true,
4299
+ description: "Property URL (https://example.com/ or sc-domain:example.com)"
3870
4300
  },
3871
- "with-sitemaps": {
4301
+ verify: {
3872
4302
  type: "boolean",
3873
4303
  default: false,
3874
- description: "Include sitemaps for each owned site"
4304
+ description: "After adding, fetch a verification token and trigger Google's validation"
4305
+ },
4306
+ method: {
4307
+ type: "string",
4308
+ alias: "m",
4309
+ description: "Verification method (used with --verify; default: META for URL-prefix, DNS_TXT for sc-domain:)"
4310
+ },
4311
+ ...OUTPUT_ARGS
4312
+ },
4313
+ async run({ args }) {
4314
+ applyOutputMode(args);
4315
+ const ctx = await createCommandContext({ needsAuth: true });
4316
+ await addSite(ctx.client, args.url).catch(gscErrorHandler);
4317
+ if (!args.verify) {
4318
+ if (args.json) {
4319
+ console.log(JSON.stringify({
4320
+ siteUrl: args.url,
4321
+ status: "added",
4322
+ verified: false
4323
+ }, null, 2));
4324
+ return;
4325
+ }
4326
+ logger.success(`Added: ${args.url}`);
4327
+ logger.info(`Property is in unverified state. Verify ownership next:`);
4328
+ const method = pickDefaultMethod(args.url);
4329
+ console.log(` \x1B[2mgscdump sites verify-token ${args.url} --method ${method}\x1B[0m`);
4330
+ return;
4331
+ }
4332
+ const method = validateMethod(args.url, args.method ?? pickDefaultMethod(args.url));
4333
+ const tokenResult = await getVerificationToken(ctx.client, args.url, method).catch(gscErrorHandler);
4334
+ if (args.json) {
4335
+ console.log(JSON.stringify({
4336
+ siteUrl: args.url,
4337
+ status: "added",
4338
+ method,
4339
+ token: tokenResult.token,
4340
+ site: tokenResult.site,
4341
+ verified: false,
4342
+ next: "Place the token, then run `gscdump sites verify <url> --method <m>`"
4343
+ }, null, 2));
4344
+ return;
4345
+ }
4346
+ logger.success(`Added: ${args.url}`);
4347
+ printPlacementInstructions(method, args.url, tokenResult.token);
4348
+ const ok = await confirm({
4349
+ message: "Token placed? Trigger Google verification now?",
4350
+ initialValue: true
4351
+ });
4352
+ if (isCancel(ok) || !ok) {
4353
+ logger.info("Skipped verification. Run `gscdump sites verify` once the token is live.");
4354
+ return;
4355
+ }
4356
+ const resource = await verifySite(ctx.client, args.url, method).catch(gscErrorHandler);
4357
+ logger.success(`Verified: ${args.url}`);
4358
+ if (resource.owners?.length) {
4359
+ console.log();
4360
+ console.log(` Owners:`);
4361
+ for (const o of resource.owners) console.log(` \x1B[90m└─\x1B[0m ${o}`);
4362
+ }
4363
+ }
4364
+ });
4365
+ const deleteCommand = defineCommand({
4366
+ meta: {
4367
+ name: "delete",
4368
+ description: "Remove a property from Search Console"
4369
+ },
4370
+ args: {
4371
+ url: {
4372
+ type: "positional",
4373
+ required: true,
4374
+ description: "Property URL to remove"
3875
4375
  },
3876
- "owner-only": {
4376
+ yes: {
3877
4377
  type: "boolean",
4378
+ alias: "y",
3878
4379
  default: false,
3879
- description: "Filter to permissionLevel=siteOwner"
4380
+ description: "Skip confirmation prompt"
3880
4381
  },
3881
- "quiet": {
4382
+ ...OUTPUT_ARGS
4383
+ },
4384
+ async run({ args }) {
4385
+ applyOutputMode(args);
4386
+ if (!args.yes && !args.json) {
4387
+ const ok = await confirm({
4388
+ message: `Remove ${args.url} from Search Console? Local synced data is unaffected.`,
4389
+ initialValue: false
4390
+ });
4391
+ if (isCancel(ok) || !ok) {
4392
+ logger.info("Cancelled");
4393
+ process.exit(0);
4394
+ }
4395
+ }
4396
+ await deleteSite((await createCommandContext({ needsAuth: true })).client, args.url).catch(gscErrorHandler);
4397
+ if (args.json) {
4398
+ console.log(JSON.stringify({
4399
+ siteUrl: args.url,
4400
+ status: "deleted"
4401
+ }, null, 2));
4402
+ return;
4403
+ }
4404
+ logger.success(`Removed: ${args.url}`);
4405
+ }
4406
+ });
4407
+ const verifyTokenCommand = defineCommand({
4408
+ meta: {
4409
+ name: "verify-token",
4410
+ description: "Get a verification token to place on the site or in DNS"
4411
+ },
4412
+ args: {
4413
+ url: {
4414
+ type: "positional",
4415
+ required: true,
4416
+ description: "Property URL"
4417
+ },
4418
+ method: {
4419
+ type: "string",
4420
+ alias: "m",
4421
+ description: "META, FILE, DNS_TXT, DNS_CNAME, ANALYTICS, TAG_MANAGER (default: META for URL-prefix, DNS_TXT for sc-domain:)"
4422
+ },
4423
+ ...OUTPUT_ARGS
4424
+ },
4425
+ async run({ args }) {
4426
+ applyOutputMode(args);
4427
+ const method = validateMethod(args.url, args.method ?? pickDefaultMethod(args.url));
4428
+ const result = await getVerificationToken((await createCommandContext({ needsAuth: true })).client, args.url, method).catch(gscErrorHandler);
4429
+ if (args.json) {
4430
+ console.log(JSON.stringify({
4431
+ siteUrl: args.url,
4432
+ method,
4433
+ token: result.token,
4434
+ site: result.site
4435
+ }, null, 2));
4436
+ return;
4437
+ }
4438
+ printPlacementInstructions(method, args.url, result.token);
4439
+ }
4440
+ });
4441
+ const verifyCommand = defineCommand({
4442
+ meta: {
4443
+ name: "verify",
4444
+ description: "Trigger verification — Google fetches/validates the token you placed"
4445
+ },
4446
+ args: {
4447
+ url: {
4448
+ type: "positional",
4449
+ required: true,
4450
+ description: "Property URL"
4451
+ },
4452
+ method: {
4453
+ type: "string",
4454
+ alias: "m",
4455
+ description: "Verification method to validate (must match the one used for verify-token)"
4456
+ },
4457
+ ...OUTPUT_ARGS
4458
+ },
4459
+ async run({ args }) {
4460
+ applyOutputMode(args);
4461
+ const method = validateMethod(args.url, args.method ?? pickDefaultMethod(args.url));
4462
+ const resource = await verifySite((await createCommandContext({ needsAuth: true })).client, args.url, method).catch(gscErrorHandler);
4463
+ if (args.json) {
4464
+ console.log(JSON.stringify({
4465
+ siteUrl: args.url,
4466
+ method,
4467
+ resource
4468
+ }, null, 2));
4469
+ return;
4470
+ }
4471
+ logger.success(`Verified: ${args.url}`);
4472
+ if (resource.owners?.length) {
4473
+ console.log();
4474
+ console.log(` Owners:`);
4475
+ for (const o of resource.owners) console.log(` \x1B[90m└─\x1B[0m ${o}`);
4476
+ }
4477
+ }
4478
+ });
4479
+ const verifyGetCommand = defineCommand({
4480
+ meta: {
4481
+ name: "verify-get",
4482
+ description: "Get a single verified WebResource by id"
4483
+ },
4484
+ args: {
4485
+ id: {
4486
+ type: "positional",
4487
+ required: true,
4488
+ description: "WebResource id (from `sites verify-list`)"
4489
+ },
4490
+ ...OUTPUT_ARGS
4491
+ },
4492
+ async run({ args }) {
4493
+ applyOutputMode(args);
4494
+ const resource = await getVerifiedSite((await createCommandContext({ needsAuth: true })).client, args.id).catch(gscErrorHandler);
4495
+ if (args.json) {
4496
+ console.log(JSON.stringify(resource, null, 2));
4497
+ return;
4498
+ }
4499
+ const ident = resource.site?.identifier ?? resource.id ?? "?";
4500
+ const type = resource.site?.type === "INET_DOMAIN" ? "domain" : "site";
4501
+ console.log();
4502
+ console.log(` \x1B[1m${ident}\x1B[0m \x1B[90m(${type})\x1B[0m`);
4503
+ console.log(` \x1B[90mid:\x1B[0m ${resource.id ?? "?"}`);
4504
+ if (resource.owners?.length) {
4505
+ console.log(` Owners:`);
4506
+ for (const o of resource.owners) console.log(` \x1B[90m└─\x1B[0m ${o}`);
4507
+ }
4508
+ console.log();
4509
+ }
4510
+ });
4511
+ const unverifyCommand = defineCommand({
4512
+ meta: {
4513
+ name: "unverify",
4514
+ description: "Drop your verified ownership of a WebResource (remove the placed token first!)"
4515
+ },
4516
+ args: {
4517
+ id: {
4518
+ type: "positional",
4519
+ required: true,
4520
+ description: "WebResource id (from `sites verify-list`)"
4521
+ },
4522
+ yes: {
3882
4523
  type: "boolean",
3883
- alias: "q",
4524
+ alias: "y",
3884
4525
  default: false,
3885
- description: "Suppress info/success output"
3886
- }
4526
+ description: "Skip confirmation prompt"
4527
+ },
4528
+ ...OUTPUT_ARGS
3887
4529
  },
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);
4023
- }
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
- }
4530
+ async run({ args }) {
4531
+ applyOutputMode(args);
4532
+ if (!args.yes && !args.json) {
4533
+ const ok = await confirm({
4534
+ message: `Unverify WebResource ${args.id}? Remove any placed verification token first or Google may re-verify.`,
4535
+ initialValue: false
4536
+ });
4537
+ if (isCancel(ok) || !ok) {
4538
+ logger.info("Cancelled");
4539
+ process.exit(0);
4071
4540
  }
4072
- })
4541
+ }
4542
+ await unverifySite((await createCommandContext({ needsAuth: true })).client, args.id).catch(gscErrorHandler);
4543
+ if (args.json) {
4544
+ console.log(JSON.stringify({
4545
+ id: args.id,
4546
+ status: "unverified"
4547
+ }, null, 2));
4548
+ return;
4549
+ }
4550
+ logger.success(`Unverified: ${args.id}`);
4551
+ }
4552
+ });
4553
+ const verifyListCommand = defineCommand({
4554
+ meta: {
4555
+ name: "verify-list",
4556
+ description: "List verified WebResources from the Site Verification API (distinct from Search Console properties)"
4073
4557
  },
4558
+ args: { ...OUTPUT_ARGS },
4074
4559
  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;
4560
+ applyOutputMode(args);
4561
+ const resources = await listVerifiedSites((await createCommandContext({ needsAuth: true })).client).catch(gscErrorHandler);
4562
+ if (args.json) {
4563
+ console.log(JSON.stringify(resources, null, 2));
4564
+ return;
4565
+ }
4566
+ if (resources.length === 0) {
4567
+ logger.warn("No verified WebResources found");
4568
+ return;
4569
+ }
4570
+ logger.success(`${resources.length} verified WebResources:`);
4571
+ console.log();
4572
+ for (const r of resources) {
4573
+ const id = r.id ?? "?";
4574
+ const site = r.site;
4575
+ const ident = site?.identifier ?? id;
4576
+ const type = site?.type === "INET_DOMAIN" ? "domain" : "site";
4577
+ console.log(` \x1B[1m${ident}\x1B[0m \x1B[90m(${type})\x1B[0m`);
4578
+ if (r.owners?.length) for (const o of r.owners) console.log(` \x1B[90m└─\x1B[0m ${o}`);
4579
+ }
4580
+ }
4581
+ });
4582
+ const getCommand = defineCommand({
4583
+ meta: {
4584
+ name: "get",
4585
+ description: "Show a single property's permissionLevel from the sites list"
4586
+ },
4587
+ args: {
4588
+ url: {
4589
+ type: "positional",
4590
+ required: true,
4591
+ description: "Property URL"
4592
+ },
4593
+ ...OUTPUT_ARGS
4594
+ },
4595
+ async run({ args }) {
4596
+ applyOutputMode(args);
4597
+ const site = (await (await createCommandContext({ needsAuth: true })).loadSites()).find((s) => s.siteUrl === args.url);
4598
+ if (!site) {
4081
4599
  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
- }
4600
+ console.log(JSON.stringify(null));
4601
+ process.exit(1);
4106
4602
  }
4603
+ logger.error(`Not found: ${args.url}`);
4604
+ process.exit(1);
4605
+ }
4606
+ if (args.json) {
4607
+ console.log(JSON.stringify(site, null, 2));
4107
4608
  return;
4108
4609
  }
4109
- const all = await ctx.loadSites();
4610
+ const perm = site.permissionLevel === "siteOwner" ? "\x1B[32m" : "\x1B[90m";
4611
+ console.log();
4612
+ console.log(` \x1B[1m${site.siteUrl}\x1B[0m`);
4613
+ console.log(` Permission: ${perm}${site.permissionLevel}\x1B[0m`);
4614
+ console.log();
4615
+ }
4616
+ });
4617
+ const LIST_ARGS = {
4618
+ ...OUTPUT_ARGS,
4619
+ "with-sitemaps": {
4620
+ type: "boolean",
4621
+ default: false,
4622
+ description: "Include sitemaps for each owned site"
4623
+ },
4624
+ "owner-only": {
4625
+ type: "boolean",
4626
+ default: false,
4627
+ description: "Filter to permissionLevel=siteOwner"
4628
+ }
4629
+ };
4630
+ async function runListSites(args) {
4631
+ applyOutputMode(args);
4632
+ const ctx = await createCommandContext({ needsAuth: true });
4633
+ const ownerOnly = Boolean(args["owner-only"]);
4634
+ if (args["with-sitemaps"]) {
4635
+ const all = await fetchSitesWithSitemaps(ctx.client).catch(gscErrorHandler);
4110
4636
  const sites = ownerOnly ? all.filter((s) => s.permissionLevel === "siteOwner") : all;
4111
4637
  if (args.json) {
4112
- console.log(JSON.stringify(sites, null, 2));
4638
+ const enriched = sites.map((s) => ({
4639
+ ...s,
4640
+ sitemapCounts: {
4641
+ total: s.sitemaps.length,
4642
+ pending: s.sitemaps.filter((sm) => sm.isPending).length,
4643
+ errored: s.sitemaps.filter((sm) => Number(sm.errors) > 0).length
4644
+ }
4645
+ }));
4646
+ console.log(JSON.stringify(enriched, null, 2));
4113
4647
  return;
4114
4648
  }
4115
4649
  if (sites.length === 0) {
4116
4650
  logger.warn(ownerOnly ? "No owned sites found" : "No verified sites found");
4117
4651
  return;
4118
4652
  }
4119
- logger.success(`Found ${sites.length} ${ownerOnly ? "owned " : ""}sites:`);
4653
+ logger.success(`Found ${sites.length} ${ownerOnly ? "owned" : "verified"} sites:`);
4120
4654
  console.log();
4121
4655
  for (const site of sites) {
4122
4656
  const perm = site.permissionLevel === "siteOwner" ? "\x1B[32m" : "\x1B[90m";
4123
4657
  console.log(` ${site.siteUrl} ${perm}(${site.permissionLevel})\x1B[0m`);
4658
+ for (const sm of site.sitemaps) {
4659
+ const pending = sm.isPending ? " \x1B[33m(pending)\x1B[0m" : "";
4660
+ console.log(` \x1B[90m└─\x1B[0m ${sm.path}${pending}`);
4661
+ }
4124
4662
  }
4663
+ return;
4664
+ }
4665
+ const all = await ctx.loadSites();
4666
+ const sites = ownerOnly ? all.filter((s) => s.permissionLevel === "siteOwner") : all;
4667
+ if (args.json) {
4668
+ console.log(JSON.stringify(sites, null, 2));
4669
+ return;
4670
+ }
4671
+ if (sites.length === 0) {
4672
+ logger.warn(ownerOnly ? "No owned sites found" : "No verified sites found");
4673
+ return;
4674
+ }
4675
+ logger.success(`Found ${sites.length} ${ownerOnly ? "owned " : ""}sites:`);
4676
+ console.log();
4677
+ for (const site of sites) {
4678
+ const perm = site.permissionLevel === "siteOwner" ? "\x1B[32m" : "\x1B[90m";
4679
+ console.log(` ${site.siteUrl} ${perm}(${site.permissionLevel})\x1B[0m`);
4680
+ }
4681
+ }
4682
+ const sitesCommand = defineCommand({
4683
+ meta: {
4684
+ name: "sites",
4685
+ description: "List GSC sites; manage properties (add/delete) and verify ownership"
4686
+ },
4687
+ args: LIST_ARGS,
4688
+ subCommands: {
4689
+ "list": defineCommand({
4690
+ meta: {
4691
+ name: "list",
4692
+ description: "List GSC sites (alias of bare `sites`)"
4693
+ },
4694
+ args: LIST_ARGS,
4695
+ async run({ args }) {
4696
+ await runListSites(args);
4697
+ }
4698
+ }),
4699
+ "add": addCommand,
4700
+ "delete": deleteCommand,
4701
+ "get": getCommand,
4702
+ "verify-token": verifyTokenCommand,
4703
+ "verify": verifyCommand,
4704
+ "verify-list": verifyListCommand,
4705
+ "verify-get": verifyGetCommand,
4706
+ "unverify": unverifyCommand
4707
+ },
4708
+ async run({ args }) {
4709
+ await runListSites(args);
4125
4710
  }
4126
4711
  });
4127
4712
  const compactCommand = defineCommand({
@@ -4152,20 +4737,10 @@ const compactCommand = defineCommand({
4152
4737
  default: false,
4153
4738
  description: "Report tier counts per (table, site) without compacting"
4154
4739
  },
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
- }
4740
+ ...OUTPUT_ARGS
4166
4741
  },
4167
4742
  async run({ args }) {
4168
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
4743
+ const { json } = applyOutputMode(args);
4169
4744
  const store = (await createCommandContext({ needsStore: true })).store;
4170
4745
  const siteId = args.site ? store.siteIdFor(String(args.site)) : void 0;
4171
4746
  const dryRun = Boolean(args["dry-run"]);
@@ -4187,7 +4762,7 @@ const compactCommand = defineCommand({
4187
4762
  ...countByTier(group)
4188
4763
  });
4189
4764
  }
4190
- if (args.json) {
4765
+ if (json) {
4191
4766
  console.log(JSON.stringify({
4192
4767
  thresholds,
4193
4768
  plan: report
@@ -4222,7 +4797,7 @@ const compactCommand = defineCommand({
4222
4797
  });
4223
4798
  }
4224
4799
  }
4225
- if (args.json) {
4800
+ if (json) {
4226
4801
  console.log(JSON.stringify({
4227
4802
  thresholds,
4228
4803
  compacted: summary
@@ -4314,20 +4889,10 @@ const exportCommand = defineCommand({
4314
4889
  default: false,
4315
4890
  description: "Overwrite the output file if it already exists"
4316
4891
  },
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
- }
4892
+ ...OUTPUT_ARGS
4328
4893
  },
4329
4894
  async run({ args }) {
4330
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
4895
+ const { json } = applyOutputMode(args);
4331
4896
  const store = (await createCommandContext({ needsStore: true })).store;
4332
4897
  const siteId = args.site ? store.siteIdFor(args.site) : void 0;
4333
4898
  const result = await exportToDuckDB({
@@ -4338,7 +4903,7 @@ const exportCommand = defineCommand({
4338
4903
  outPath: args.out,
4339
4904
  force: args.force
4340
4905
  });
4341
- if (args.json) {
4906
+ if (json) {
4342
4907
  console.log(JSON.stringify(result, null, 2));
4343
4908
  return;
4344
4909
  }
@@ -4374,20 +4939,10 @@ const gcCommand = defineCommand({
4374
4939
  default: false,
4375
4940
  description: "List retired manifest entries past the grace window without deleting"
4376
4941
  },
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
- }
4942
+ ...OUTPUT_ARGS
4388
4943
  },
4389
4944
  async run({ args }) {
4390
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
4945
+ const { json } = applyOutputMode(args);
4391
4946
  const store = (await createCommandContext({ needsStore: true })).store;
4392
4947
  const siteId = args.site ? store.siteIdFor(String(args.site)) : void 0;
4393
4948
  const graceMs = Number(args["grace-hours"]) * 36e5;
@@ -4408,7 +4963,7 @@ const gcCommand = defineCommand({
4408
4963
  objectKey: e.objectKey
4409
4964
  });
4410
4965
  }
4411
- if (args.json) {
4966
+ if (json) {
4412
4967
  console.log(JSON.stringify({
4413
4968
  graceHours: Number(args["grace-hours"]),
4414
4969
  candidates
@@ -4425,7 +4980,7 @@ const gcCommand = defineCommand({
4425
4980
  userId: store.userId,
4426
4981
  siteId
4427
4982
  }, graceMs);
4428
- if (args.json) {
4983
+ if (json) {
4429
4984
  console.log(JSON.stringify({
4430
4985
  graceHours: Number(args["grace-hours"]),
4431
4986
  deleted: result.deleted
@@ -4446,25 +5001,15 @@ const rollupsCommand = defineCommand({
4446
5001
  description: "Rebuild post-sync rollups (daily totals, weekly totals, top-N tables) for a site"
4447
5002
  },
4448
5003
  args: {
5004
+ ...OUTPUT_ARGS,
4449
5005
  site: {
4450
5006
  type: "string",
4451
5007
  alias: "s",
4452
5008
  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
5009
  }
4465
5010
  },
4466
5011
  async run({ args }) {
4467
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
5012
+ const { json } = applyOutputMode(args);
4468
5013
  const store = (await createCommandContext({ needsStore: true })).store;
4469
5014
  const explicitSiteId = args.site ? store.siteIdFor(String(args.site)) : void 0;
4470
5015
  const allSiteIds = /* @__PURE__ */ new Set();
@@ -4477,7 +5022,7 @@ const rollupsCommand = defineCommand({
4477
5022
  for (const e of entries) if (e.siteId) allSiteIds.add(e.siteId);
4478
5023
  }
4479
5024
  if (allSiteIds.size === 0) {
4480
- if (args.json) console.log(JSON.stringify({
5025
+ if (json) console.log(JSON.stringify({
4481
5026
  sites: [],
4482
5027
  totalBytes: 0
4483
5028
  }, null, 2));
@@ -4508,11 +5053,11 @@ const rollupsCommand = defineCommand({
4508
5053
  bytes: r.bytes,
4509
5054
  objectKey: r.objectKey
4510
5055
  });
4511
- if (!args.json) console.log(` ${r.id.padEnd(20)} ${(r.bytes / 1024).toFixed(1).padStart(8)} KB ${r.objectKey}`);
5056
+ if (!json) console.log(` ${r.id.padEnd(20)} ${(r.bytes / 1024).toFixed(1).padStart(8)} KB ${r.objectKey}`);
4512
5057
  }
4513
5058
  summary.push(site);
4514
5059
  }
4515
- if (args.json) {
5060
+ if (json) {
4516
5061
  console.log(JSON.stringify({
4517
5062
  sites: summary,
4518
5063
  totalBytes
@@ -4529,24 +5074,14 @@ const statsCommand = defineCommand({
4529
5074
  description: "Show row/byte counts per table and on-disk footprint"
4530
5075
  },
4531
5076
  args: {
4532
- json: {
4533
- type: "boolean",
4534
- default: false,
4535
- description: "Output as JSON"
4536
- },
5077
+ ...OUTPUT_ARGS,
4537
5078
  site: {
4538
5079
  type: "string",
4539
5080
  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
5081
  }
4547
5082
  },
4548
5083
  async run({ args }) {
4549
- setQuiet(Boolean(args.quiet) || Boolean(args.json));
5084
+ const { json } = applyOutputMode(args);
4550
5085
  const store = (await createCommandContext({ needsStore: true })).store;
4551
5086
  let siteId;
4552
5087
  if (args.site) {
@@ -4578,7 +5113,7 @@ const statsCommand = defineCommand({
4578
5113
  files: 0,
4579
5114
  bytes: 0
4580
5115
  }));
4581
- if (args.json) {
5116
+ if (json) {
4582
5117
  const payload = {
4583
5118
  dataDir: store.dataDir,
4584
5119
  disk,
@@ -4664,11 +5199,98 @@ const storeCommand = defineCommand({
4664
5199
  description: "Manage the local DuckDB/Parquet store"
4665
5200
  },
4666
5201
  subCommands: {
4667
- stats: statsCommand,
4668
- compact: compactCommand,
4669
- gc: gcCommand,
4670
- export: exportCommand,
4671
- rollups: rollupsCommand
5202
+ "stats": statsCommand,
5203
+ "compact": compactCommand,
5204
+ "gc": gcCommand,
5205
+ "export": exportCommand,
5206
+ "rollups": rollupsCommand,
5207
+ "rm-site": defineCommand({
5208
+ meta: {
5209
+ name: "rm-site",
5210
+ description: "Delete every parquet, manifest, watermark, and sync-state record for a single site"
5211
+ },
5212
+ args: {
5213
+ site: {
5214
+ type: "positional",
5215
+ required: true,
5216
+ description: "Site URL (e.g. sc-domain:example.com)"
5217
+ },
5218
+ yes: {
5219
+ type: "boolean",
5220
+ alias: "y",
5221
+ default: false,
5222
+ description: "Skip confirmation prompt"
5223
+ },
5224
+ ...OUTPUT_ARGS
5225
+ },
5226
+ async run({ args }) {
5227
+ const { json } = applyOutputMode(args);
5228
+ const store = (await createCommandContext({ needsStore: true })).store;
5229
+ const siteId = store.siteIdFor(String(args.site));
5230
+ if (!args.yes && !json) {
5231
+ const ok = await confirm({
5232
+ message: `Delete ALL local data for ${args.site}? This is irreversible.`,
5233
+ initialValue: false
5234
+ });
5235
+ if (isCancel(ok) || !ok) {
5236
+ logger.info("Cancelled");
5237
+ process.exit(0);
5238
+ }
5239
+ }
5240
+ const result = await store.engine.purgeTenant({
5241
+ userId: store.userId,
5242
+ siteId
5243
+ });
5244
+ if (json) {
5245
+ console.log(JSON.stringify(result, null, 2));
5246
+ return;
5247
+ }
5248
+ logger.success(`Removed local data for ${args.site}`);
5249
+ console.log(` Objects deleted: ${result.objectsDeleted}`);
5250
+ console.log(` Manifest entries: ${result.entriesRemoved}`);
5251
+ console.log(` Watermarks: ${result.watermarksRemoved}`);
5252
+ console.log(` Sync states: ${result.syncStatesRemoved}`);
5253
+ }
5254
+ }),
5255
+ "reset": defineCommand({
5256
+ meta: {
5257
+ name: "reset",
5258
+ description: "Wipe the entire local store (every site, every table). Irreversible."
5259
+ },
5260
+ args: {
5261
+ yes: {
5262
+ type: "boolean",
5263
+ alias: "y",
5264
+ default: false,
5265
+ description: "Skip confirmation prompt"
5266
+ },
5267
+ ...OUTPUT_ARGS
5268
+ },
5269
+ async run({ args }) {
5270
+ const { json } = applyOutputMode(args);
5271
+ const store = (await createCommandContext({ needsStore: true })).store;
5272
+ if (!args.yes && !json) {
5273
+ const ok = await confirm({
5274
+ message: `Wipe the entire local store under ${store.dataDir}? This deletes data for ALL sites.`,
5275
+ initialValue: false
5276
+ });
5277
+ if (isCancel(ok) || !ok) {
5278
+ logger.info("Cancelled");
5279
+ process.exit(0);
5280
+ }
5281
+ }
5282
+ const result = await store.engine.purgeTenant({ userId: store.userId });
5283
+ if (json) {
5284
+ console.log(JSON.stringify(result, null, 2));
5285
+ return;
5286
+ }
5287
+ logger.success("Local store reset");
5288
+ console.log(` Objects deleted: ${result.objectsDeleted}`);
5289
+ console.log(` Manifest entries: ${result.entriesRemoved}`);
5290
+ console.log(` Watermarks: ${result.watermarksRemoved}`);
5291
+ console.log(` Sync states: ${result.syncStatesRemoved}`);
5292
+ }
5293
+ })
4672
5294
  }
4673
5295
  });
4674
5296
  const DEFAULT_TABLES = [
@@ -4844,12 +5466,7 @@ const syncCommand = defineCommand({
4844
5466
  type: "boolean",
4845
5467
  description: "Sync the last 450 days (full GSC history)"
4846
5468
  },
4847
- "quiet": {
4848
- type: "boolean",
4849
- alias: "q",
4850
- default: false,
4851
- description: "Suppress progress output"
4852
- },
5469
+ ...OUTPUT_ARGS,
4853
5470
  "force": {
4854
5471
  type: "boolean",
4855
5472
  default: false,
@@ -4860,11 +5477,6 @@ const syncCommand = defineCommand({
4860
5477
  default: false,
4861
5478
  description: "Print watermarks + sync-state summary instead of syncing"
4862
5479
  },
4863
- "json": {
4864
- type: "boolean",
4865
- default: false,
4866
- description: "With --status: emit JSON"
4867
- },
4868
5480
  "concurrency": {
4869
5481
  type: "string",
4870
5482
  alias: "c",
@@ -4887,8 +5499,9 @@ const syncCommand = defineCommand({
4887
5499
  }
4888
5500
  },
4889
5501
  async run({ args }) {
5502
+ const { json, quiet } = applyOutputMode(args);
4890
5503
  if (args.status) {
4891
- await printSyncStatus(await loadConfig(), args.site ? String(args.site) : void 0, Boolean(args.json));
5504
+ await printSyncStatus(await loadConfig(), args.site ? String(args.site) : void 0, json);
4892
5505
  return;
4893
5506
  }
4894
5507
  const ctx = await createCommandContext({
@@ -4923,7 +5536,7 @@ const syncCommand = defineCommand({
4923
5536
  logger.warn(`All requested types (${requestedTypes.join(", ")}) are marked empty for this site. Pass --force-types to re-probe.`);
4924
5537
  return;
4925
5538
  }
4926
- if (skippedTypes.length > 0 && !args.quiet) logger.info(`Skipping ${skippedTypes.join(", ")} (marked empty for this site; pass --force-types to re-probe).`);
5539
+ if (skippedTypes.length > 0 && !quiet) logger.info(`Skipping ${skippedTypes.join(", ")} (marked empty for this site; pass --force-types to re-probe).`);
4927
5540
  const endDate = args.end ? String(args.end) : daysAgo(DEFAULT_PENDING_DAYS);
4928
5541
  let startDate;
4929
5542
  if (args.start) startDate = String(args.start);
@@ -4953,7 +5566,7 @@ const syncCommand = defineCommand({
4953
5566
  return;
4954
5567
  }
4955
5568
  args.force = true;
4956
- if (!args.quiet) logger.info(`--retry-failed: ${dates.length} date(s) to retry`);
5569
+ if (!quiet) logger.info(`--retry-failed: ${dates.length} date(s) to retry`);
4957
5570
  }
4958
5571
  if (args["dry-run"]) {
4959
5572
  const plan = [];
@@ -4962,7 +5575,7 @@ const syncCommand = defineCommand({
4962
5575
  searchType: type,
4963
5576
  date
4964
5577
  });
4965
- if (args.json) {
5578
+ if (json) {
4966
5579
  console.log(JSON.stringify({
4967
5580
  siteUrl,
4968
5581
  range: {
@@ -4985,7 +5598,7 @@ const syncCommand = defineCommand({
4985
5598
  logger.info("Pass without --dry-run to execute.");
4986
5599
  return;
4987
5600
  }
4988
- if (!args.quiet) {
5601
+ if (!quiet) {
4989
5602
  logger.info(`Syncing ${siteUrl} (${tables.join(", ")}) [${types.join(", ")}] → ${displayPath(store.dataDir)}`);
4990
5603
  logger.info(`Range: ${startDate} → ${endDate} (${dates.length} days)`);
4991
5604
  }
@@ -5002,7 +5615,7 @@ const syncCommand = defineCommand({
5002
5615
  label
5003
5616
  });
5004
5617
  }
5005
- const progress = createProgressTracker(dates.length * jobs.length, Boolean(args.quiet));
5618
+ const progress = createProgressTracker(dates.length * jobs.length, quiet);
5006
5619
  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
5620
  else {
5008
5621
  const results = await Promise.all(jobs.map((job) => syncTable(store, siteUrl, job.table, job.type, dates, client, concurrency, args.force, progress)));
@@ -5012,7 +5625,7 @@ const syncCommand = defineCommand({
5012
5625
  }
5013
5626
  progress.done();
5014
5627
  const seconds = ((Date.now() - start) / 1e3).toFixed(1);
5015
- if (!args.quiet) {
5628
+ if (!quiet) {
5016
5629
  logger.success(`Synced ${siteUrl} in ${seconds}s`);
5017
5630
  for (const [t, n] of Object.entries(totals)) {
5018
5631
  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 +5654,7 @@ const syncCommand = defineCommand({
5041
5654
  userId: store.userId,
5042
5655
  siteId
5043
5656
  }, 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).`);
5657
+ if (!quiet) logger.info(`Marked empty for future syncs: ${toMark.join(", ")} (0 rows across ${dates.length} days; pass --force-types to re-probe).`);
5045
5658
  }
5046
5659
  }
5047
5660
  if (forceTypes && emptyTypesDoc.emptyTypes.length > 0) {
@@ -5052,13 +5665,13 @@ const syncCommand = defineCommand({
5052
5665
  userId: store.userId,
5053
5666
  siteId
5054
5667
  }, toClear);
5055
- if (!args.quiet) logger.info(`Cleared empty markers for: ${toClear.join(", ")} (re-probe found data).`);
5668
+ if (!quiet) logger.info(`Cleared empty markers for: ${toClear.join(", ")} (re-probe found data).`);
5056
5669
  }
5057
5670
  }
5058
5671
  const noRollups = Boolean(args["no-rollups"]);
5059
5672
  const anyRowsSynced = Object.values(totals).some((t) => t.rows > 0);
5060
5673
  if (!noRollups && anyRowsSynced) {
5061
- if (!args.quiet) logger.info(`Rebuilding rollups for [${siteId}] (${DEFAULT_ROLLUPS.length} rollups)…`);
5674
+ if (!quiet) logger.info(`Rebuilding rollups for [${siteId}] (${DEFAULT_ROLLUPS.length} rollups)…`);
5062
5675
  const rollupStart = Date.now();
5063
5676
  const results = await rebuildRollups({
5064
5677
  engine: store.engine,
@@ -5072,7 +5685,7 @@ const syncCommand = defineCommand({
5072
5685
  logger.warn(`Rollup rebuild failed: ${err.message}`);
5073
5686
  return [];
5074
5687
  });
5075
- if (!args.quiet && results.length > 0) {
5688
+ if (!quiet && results.length > 0) {
5076
5689
  const kb = results.reduce((a, r) => a + r.bytes, 0) / 1024;
5077
5690
  const ms = Date.now() - rollupStart;
5078
5691
  logger.success(`Rebuilt ${results.length} rollup(s) in ${ms}ms — ${kb.toFixed(1)} KB`);
@@ -5160,10 +5773,11 @@ function shouldShowSplash() {
5160
5773
  function applyGlobalArgs() {
5161
5774
  const argv = process.argv;
5162
5775
  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));
5776
+ const profile = pluckArgValue(argv, "--profile");
5777
+ applyProfileFromCli({
5778
+ configDir: pluckArgValue(argv, "--config-dir") ?? process.env.GSCDUMP_CONFIG_DIR ?? null,
5779
+ profile
5780
+ });
5167
5781
  if (argv.includes("-v") && !argv.includes("--version")) {
5168
5782
  const i = argv.indexOf("-v");
5169
5783
  argv[i] = "--version";
@@ -5207,6 +5821,7 @@ runMain(defineCommand({
5207
5821
  analyze: analyzeCommand,
5208
5822
  auth: authCommand,
5209
5823
  config: configCommand,
5824
+ profile: profileCommand,
5210
5825
  doctor: doctorCommand,
5211
5826
  mcp: mcpCommand
5212
5827
  },