@gscdump/cli 0.17.5 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { a as loadConfig, c as setConfigDir, i as getConfigPath, l as __exportAll, n as defaultDataDir, o as resolveDataDir, r as getConfigDir, s as saveConfig } from "./_chunks/config.mjs";
3
2
  import process from "node:process";
4
3
  import { defineCommand, runMain } from "citty";
5
4
  import { defaultAnalyzerRegistry } from "@gscdump/analysis/registry";
@@ -15,9 +14,9 @@ import { createServer } from "node:http";
15
14
  import { JWT, OAuth2Client } from "google-auth-library";
16
15
  import { ofetch } from "ofetch";
17
16
  import fs$1 from "node:fs";
18
- import { Buffer } from "node:buffer";
17
+ import { Buffer as Buffer$1 } from "node:buffer";
19
18
  import { createConsola } from "consola";
20
- import { SearchTypes, and, between, contains, country, date, device, eq, gsc, notRegex, page, query, regex, searchAppearance } from "gscdump/query";
19
+ import { SearchTypes, and, between, contains, country, date, device, eq, gsc, hour, notRegex, page, query, regex, searchAppearance } from "gscdump/query";
21
20
  import { createNodeHarness } from "@gscdump/engine/node";
22
21
  import { TABLE_DIMS, transformGscRow } from "@gscdump/engine/ingest";
23
22
  import { allTables, inferTable } from "@gscdump/engine/schema";
@@ -31,6 +30,56 @@ import { resolveWindow } from "@gscdump/engine/period";
31
30
  import { inferLegacyTier } from "@gscdump/engine";
32
31
  import { DEFAULT_ROLLUPS, rebuildRollups } from "@gscdump/engine/rollups";
33
32
  import { filesystemStats } from "@gscdump/engine/filesystem";
33
+ var __defProp = Object.defineProperty;
34
+ var __exportAll = (all, no_symbols) => {
35
+ let target = {};
36
+ for (var name in all) __defProp(target, name, {
37
+ get: all[name],
38
+ enumerable: true
39
+ });
40
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
41
+ return target;
42
+ };
43
+ var config_exports = /* @__PURE__ */ __exportAll({
44
+ defaultDataDir: () => defaultDataDir,
45
+ getConfigDir: () => getConfigDir,
46
+ getConfigPath: () => getConfigPath,
47
+ loadConfig: () => loadConfig,
48
+ resolveDataDir: () => resolveDataDir,
49
+ saveConfig: () => saveConfig,
50
+ setConfigDir: () => setConfigDir
51
+ });
52
+ let configDir = path.join(os.homedir(), ".config", "gscdump");
53
+ function setConfigDir(dir) {
54
+ configDir = dir;
55
+ }
56
+ function getConfigDir() {
57
+ return configDir;
58
+ }
59
+ function defaultDataDir() {
60
+ return path.join(os.homedir(), ".gscdump", "data");
61
+ }
62
+ function resolveDataDir(config) {
63
+ return expandTilde(config.dataDir ?? defaultDataDir());
64
+ }
65
+ function expandTilde(p) {
66
+ if (p === "~") return os.homedir();
67
+ if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
68
+ return p;
69
+ }
70
+ async function loadConfig() {
71
+ return fs.readFile(path.join(configDir, "config.json"), "utf-8").then((data) => JSON.parse(data)).catch(() => ({}));
72
+ }
73
+ async function saveConfig(config) {
74
+ await fs.mkdir(configDir, {
75
+ recursive: true,
76
+ mode: 448
77
+ });
78
+ await fs.writeFile(path.join(configDir, "config.json"), JSON.stringify(config, null, 2), { mode: 384 });
79
+ }
80
+ function getConfigPath() {
81
+ return path.join(configDir, "config.json");
82
+ }
34
83
  const ENV_LINE_RE$1 = /^([^=]+)=(.*)$/;
35
84
  function parseEnvFile(envPath) {
36
85
  let content;
@@ -73,9 +122,16 @@ function loadEnvFromCwd() {
73
122
  }
74
123
  return applied;
75
124
  }
76
- var version = "0.17.5";
125
+ var version = "0.18.1";
77
126
  const ALL_SEARCH_TYPES$1 = Object.values(SearchTypes);
78
127
  const VERSION = version;
128
+ function noSubcommandSelected(parent, subNames) {
129
+ const idx = process.argv.indexOf(parent);
130
+ if (idx < 0) return true;
131
+ const next = process.argv[idx + 1];
132
+ if (!next) return true;
133
+ return !subNames.includes(next);
134
+ }
79
135
  const baseLogger = createConsola({
80
136
  stdout: process.stderr,
81
137
  stderr: process.stderr
@@ -128,7 +184,7 @@ function wrapStdoutForNoColor() {
128
184
  const original = process.stdout.write.bind(process.stdout);
129
185
  process.stdout.write = ((chunk, ...rest) => {
130
186
  if (typeof chunk === "string") chunk = chunk.replace(ANSI_RE, "");
131
- else if (chunk instanceof Uint8Array) chunk = Buffer.from(chunk).toString("utf8").replace(ANSI_RE, "");
187
+ else if (chunk instanceof Uint8Array) chunk = Buffer$1.from(chunk).toString("utf8").replace(ANSI_RE, "");
132
188
  return original(chunk, ...rest);
133
189
  });
134
190
  }
@@ -201,7 +257,7 @@ async function readUrlList$1(args) {
201
257
  if (!process.stdin.isTTY) {
202
258
  const chunks = [];
203
259
  for await (const chunk of process.stdin) chunks.push(chunk);
204
- return Buffer.concat(chunks).toString("utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
260
+ return Buffer$1.concat(chunks).toString("utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
205
261
  }
206
262
  return [];
207
263
  }
@@ -335,8 +391,10 @@ async function getAuthCredentials(interactive) {
335
391
  const envClientId = process.env.GOOGLE_CLIENT_ID;
336
392
  const envClientSecret = process.env.GOOGLE_CLIENT_SECRET;
337
393
  if (envClientId && envClientSecret) {
338
- logger.info("Using OAuth client from env");
339
- console.log(` \x1B[90m${envClientId}\x1B[0m`);
394
+ if (interactive) {
395
+ logger.info("Using OAuth client from env");
396
+ console.log(` \x1B[90m${envClientId}\x1B[0m`);
397
+ }
340
398
  return {
341
399
  clientId: envClientId,
342
400
  clientSecret: envClientSecret
@@ -344,8 +402,10 @@ async function getAuthCredentials(interactive) {
344
402
  }
345
403
  const config = await loadConfig();
346
404
  if (config.clientId && config.clientSecret) {
347
- logger.info(`Using OAuth client from ${displayPath(`${getConfigDir()}/config.json`)}`);
348
- console.log(` \x1B[90m${config.clientId}\x1B[0m`);
405
+ if (interactive) {
406
+ logger.info(`Using OAuth client from ${displayPath(`${getConfigDir()}/config.json`)}`);
407
+ console.log(` \x1B[90m${config.clientId}\x1B[0m`);
408
+ }
349
409
  return {
350
410
  clientId: config.clientId,
351
411
  clientSecret: config.clientSecret
@@ -429,13 +489,18 @@ async function getAuthCodeViaLoopback(authUrl) {
429
489
  console.log(` \x1B[90mIf browser doesn't open, visit:\x1B[0m`);
430
490
  console.log(` \x1B[36m${fullAuthUrl}\x1B[0m`);
431
491
  console.log();
492
+ console.log(` \x1B[90mIf Google says "redirect_uri_mismatch", your OAuth client is`);
493
+ console.log(` not a "Desktop application" type. Create a Desktop client at`);
494
+ console.log(` https://console.cloud.google.com/apis/credentials, then run`);
495
+ console.log(` \`gscdump init --force\` with the new ID/secret.\x1B[0m`);
496
+ console.log();
432
497
  import("open").then(({ default: open }) => open(fullAuthUrl)).catch(() => {
433
498
  logger.warn("Could not open browser automatically");
434
499
  });
435
500
  });
436
501
  server.on("error", (err) => settle(() => reject(err)));
437
502
  timeoutId = setTimeout(() => {
438
- settle(() => reject(/* @__PURE__ */ new Error("Authorization timed out")));
503
+ settle(() => reject(/* @__PURE__ */ new Error("Authorization timed out. If Google showed \"redirect_uri_mismatch\", your OAuth client must be type \"Desktop application\" (create one at https://console.cloud.google.com/apis/credentials and run `gscdump init --force`).")));
439
504
  }, 300 * 1e3);
440
505
  });
441
506
  }
@@ -455,23 +520,39 @@ async function authenticate(credentials, interactive, opts = {}) {
455
520
  return oauth2Client;
456
521
  }
457
522
  const existingTokens = !opts.force ? await loadTokens() : null;
523
+ let refreshFailed = false;
524
+ let refreshError = null;
458
525
  if (existingTokens) {
459
526
  oauth2Client.setCredentials(existingTokens);
460
527
  if (existingTokens.expiry_date && existingTokens.expiry_date < Date.now()) {
461
- const { credentials: newTokens } = await oauth2Client.refreshAccessToken().catch(() => ({ credentials: null }));
462
- if (newTokens) {
463
- await saveTokens(newTokens);
464
- oauth2Client.setCredentials(newTokens);
465
- logger.success("Token refreshed");
528
+ const result = await oauth2Client.refreshAccessToken().then((r) => ({
529
+ credentials: r.credentials,
530
+ error: null
531
+ })).catch((e) => ({
532
+ credentials: null,
533
+ error: e
534
+ }));
535
+ if (result.credentials) {
536
+ await saveTokens(result.credentials);
537
+ oauth2Client.setCredentials(result.credentials);
538
+ if (interactive) logger.success("Token refreshed");
466
539
  return oauth2Client;
467
540
  }
541
+ refreshFailed = true;
542
+ refreshError = result.error;
468
543
  } else {
469
- logger.success("Using saved credentials");
544
+ if (interactive) logger.success("Using saved credentials");
470
545
  return oauth2Client;
471
546
  }
472
547
  }
473
548
  if (!interactive) {
474
- logger.error("No saved tokens. Run interactively first to authenticate.");
549
+ if (refreshFailed) {
550
+ logger.error(`Token refresh failed${refreshError ? `: ${refreshError.message}` : ""}`);
551
+ logger.info("Refresh token may be revoked or expired. Run `gscdump auth login` to re-authenticate.");
552
+ } else {
553
+ logger.error("Not authenticated");
554
+ logger.info("Run `gscdump auth login` (or `gscdump init` for full setup).");
555
+ }
475
556
  process.exit(1);
476
557
  }
477
558
  if (opts.noBrowser) {
@@ -1125,6 +1206,183 @@ const analyzeCommand = defineCommand({
1125
1206
  ...Object.fromEntries(ANALYSIS_TOOLS.map((tool) => [tool, makeToolCommand(tool)]))
1126
1207
  }
1127
1208
  });
1209
+ const ENV_LINE_RE = /^([^=]+)=(.*)$/;
1210
+ async function promptDataDir(existing) {
1211
+ const fallback = existing ?? defaultDataDir();
1212
+ const answer = await text({
1213
+ message: "Where should Parquet data be stored?",
1214
+ placeholder: fallback,
1215
+ defaultValue: fallback
1216
+ });
1217
+ if (isCancel(answer)) process.exit(1);
1218
+ return String(answer) || fallback;
1219
+ }
1220
+ async function loadEnvFile() {
1221
+ const envPath = path.join(process.cwd(), ".env");
1222
+ const content = await fs.readFile(envPath, "utf-8").catch(() => null);
1223
+ if (!content) return null;
1224
+ const env = {};
1225
+ for (const line of content.split("\n")) {
1226
+ const trimmed = line.trim();
1227
+ if (!trimmed || trimmed.startsWith("#")) continue;
1228
+ const match = trimmed.match(ENV_LINE_RE);
1229
+ if (match) {
1230
+ const key = match[1].trim();
1231
+ let value = match[2].trim();
1232
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
1233
+ env[key] = value;
1234
+ }
1235
+ }
1236
+ return env;
1237
+ }
1238
+ const initCommand = defineCommand({
1239
+ meta: {
1240
+ name: "init",
1241
+ description: "Set up GSCDump authentication"
1242
+ },
1243
+ args: {
1244
+ "force": {
1245
+ type: "boolean",
1246
+ alias: "f",
1247
+ description: "Force re-initialization"
1248
+ },
1249
+ "no-store": {
1250
+ type: "boolean",
1251
+ default: false,
1252
+ description: "Skip dataDir prompt (auth-only setup)"
1253
+ },
1254
+ ...OUTPUT_ARGS
1255
+ },
1256
+ async run({ args }) {
1257
+ applyOutputMode(args);
1258
+ const config = await loadConfig();
1259
+ if (config.clientId && config.clientSecret && !args.force) {
1260
+ logger.info("Already configured");
1261
+ const tokens = await loadTokens();
1262
+ if (!tokens) logger.info("No saved tokens. Run `gscdump auth login` to authenticate.");
1263
+ else {
1264
+ const isExpired = tokens.expiry_date && tokens.expiry_date < Date.now();
1265
+ if (isExpired && tokens.refresh_token) logger.info("Tokens expired but refresh available. Any live command will auto-refresh, or run `gscdump auth refresh`.");
1266
+ else if (isExpired) logger.warn("Tokens expired without a refresh token. Run `gscdump auth login` to re-authenticate.");
1267
+ else logger.info("Next: `gscdump sites` to list properties, or `gscdump query --help`.");
1268
+ }
1269
+ logger.info("Run `gscdump init --force` to reconfigure.");
1270
+ return;
1271
+ }
1272
+ const byok = resolveBYOK();
1273
+ if (byok) {
1274
+ const dataDir = args["no-store"] ? void 0 : await promptDataDir(config.dataDir);
1275
+ await saveConfig({
1276
+ ...config,
1277
+ dataDir: dataDir ?? config.dataDir
1278
+ });
1279
+ logger.success(`BYOK detected (${typeof byok === "string" ? "access-token" : "refresh-token"}) — auth setup skipped`);
1280
+ logger.success("Setup complete! Run gscdump to get started.");
1281
+ return;
1282
+ }
1283
+ const envFile = await loadEnvFile();
1284
+ const envCid = envFile?.GSC_CLIENT_ID ?? envFile?.GOOGLE_CLIENT_ID;
1285
+ const envSec = envFile?.GSC_CLIENT_SECRET ?? envFile?.GOOGLE_CLIENT_SECRET;
1286
+ const envRef = envFile?.GSC_REFRESH_TOKEN ?? envFile?.GOOGLE_REFRESH_TOKEN;
1287
+ const envAcc = envFile?.GSC_ACCESS_TOKEN ?? envFile?.GOOGLE_ACCESS_TOKEN;
1288
+ if (envCid && envSec && envRef) {
1289
+ logger.info("Found .env file with credentials");
1290
+ process.env.GOOGLE_CLIENT_ID = envCid;
1291
+ process.env.GOOGLE_CLIENT_SECRET = envSec;
1292
+ process.env.GOOGLE_REFRESH_TOKEN = envRef;
1293
+ if (envAcc) process.env.GOOGLE_ACCESS_TOKEN = envAcc;
1294
+ await saveConfig({
1295
+ ...config,
1296
+ clientId: envCid,
1297
+ clientSecret: envSec,
1298
+ dataDir: config.dataDir ?? defaultDataDir()
1299
+ });
1300
+ const creds = (await authenticate({
1301
+ clientId: envCid,
1302
+ clientSecret: envSec
1303
+ }, false)).credentials;
1304
+ if (creds.access_token) await saveTokens({
1305
+ access_token: creds.access_token,
1306
+ refresh_token: creds.refresh_token || envRef,
1307
+ expiry_date: creds.expiry_date
1308
+ });
1309
+ console.log();
1310
+ logger.success("Setup complete using .env credentials! Run gscdump to get started.");
1311
+ return;
1312
+ }
1313
+ console.log();
1314
+ console.log(" \x1B[1mWelcome to GSCDump!\x1B[0m");
1315
+ console.log(" \x1B[90mGoogle Search Console data extraction CLI\x1B[0m");
1316
+ console.log();
1317
+ const dataDir = args["no-store"] ? void 0 : await promptDataDir(config.dataDir);
1318
+ const credentials = await getAuthCredentials(true);
1319
+ await saveConfig({
1320
+ ...config,
1321
+ ...dataDir ? { dataDir } : {},
1322
+ clientId: credentials.clientId,
1323
+ clientSecret: credentials.clientSecret
1324
+ });
1325
+ await smokeTest(await authenticate(credentials, true));
1326
+ await maybeWriteEnvFile(credentials.clientId, credentials.clientSecret);
1327
+ console.log();
1328
+ logger.success("Setup complete! Run gscdump to get started.");
1329
+ }
1330
+ });
1331
+ async function smokeTest(oauth) {
1332
+ await runSmokeTest(oauth);
1333
+ }
1334
+ async function runSmokeTest(oauth) {
1335
+ const sites = await googleSearchConsole(oauth).sites().catch((e) => e);
1336
+ if (sites instanceof Error) {
1337
+ const msg = sites.message;
1338
+ const apiDisabledMatch = msg.match(/projects?\/(\d+)/) ?? msg.match(/project (\d+)/);
1339
+ if (/has not been used in project|API has not been used|SERVICE_DISABLED/i.test(msg)) {
1340
+ logger.error("Search Console API is not enabled for this Google Cloud project.");
1341
+ const project = apiDisabledMatch?.[1];
1342
+ const url = project ? `https://console.developers.google.com/apis/api/searchconsole.googleapis.com/overview?project=${project}` : "https://console.developers.google.com/apis/api/searchconsole.googleapis.com/overview";
1343
+ logger.info(`Enable it here, then retry: ${url}`);
1344
+ logger.info("Note: it can take a few minutes to propagate after enabling.");
1345
+ return;
1346
+ }
1347
+ if (/insufficient|scope|forbidden|403/i.test(msg)) {
1348
+ logger.warn(`Smoke test failed (likely missing scopes): ${msg}`);
1349
+ logger.info("Run `gscdump auth login --force` to re-consent with the required scopes.");
1350
+ return;
1351
+ }
1352
+ logger.warn(`Smoke test failed: ${msg}`);
1353
+ logger.info("Auth saved, but verify via `gscdump auth status` / `gscdump doctor`.");
1354
+ return;
1355
+ }
1356
+ logger.success(`Verified: ${sites.length} GSC site(s) accessible`);
1357
+ }
1358
+ async function maybeWriteEnvFile(clientId, clientSecret) {
1359
+ const tokens = await loadTokens();
1360
+ if (!tokens?.refresh_token) return;
1361
+ const wants = await confirm({
1362
+ message: "Write a `.env` file with these credentials? (handy for CI / other machines)",
1363
+ initialValue: false
1364
+ });
1365
+ if (isCancel(wants) || !wants) return;
1366
+ const envPath = path.join(process.cwd(), ".env");
1367
+ if (await fs.stat(envPath).then(() => true).catch(() => false)) {
1368
+ const overwrite = await confirm({
1369
+ message: `${displayPath(envPath)} exists. Overwrite?`,
1370
+ initialValue: false
1371
+ });
1372
+ if (isCancel(overwrite) || !overwrite) {
1373
+ logger.info(`Skipped — keep credentials at ${displayPath(envPath)} manually if needed`);
1374
+ return;
1375
+ }
1376
+ }
1377
+ const content = [
1378
+ `GSC_CLIENT_ID=${clientId}`,
1379
+ `GSC_CLIENT_SECRET=${clientSecret}`,
1380
+ `GSC_REFRESH_TOKEN=${tokens.refresh_token}`,
1381
+ ""
1382
+ ].join("\n");
1383
+ await fs.writeFile(envPath, content, { mode: 384 });
1384
+ logger.success(`Wrote ${displayPath(envPath)}`);
1385
+ }
1128
1386
  const ROOT_DIR = path.join(os.homedir(), ".config", "gscdump");
1129
1387
  const PROFILES_DIR = path.join(ROOT_DIR, "profiles");
1130
1388
  const ACTIVE_MARKER = path.join(ROOT_DIR, "active-profile");
@@ -1204,37 +1462,38 @@ async function adoptCurrentConfigAsProfile(name) {
1204
1462
  setConfigDir(targetDir);
1205
1463
  return targetDir;
1206
1464
  }
1465
+ const listCmd = defineCommand({
1466
+ meta: {
1467
+ name: "list",
1468
+ description: "List configured profiles"
1469
+ },
1470
+ args: { ...OUTPUT_ARGS },
1471
+ async run({ args }) {
1472
+ const { json } = applyOutputMode(args);
1473
+ const names = await listProfiles();
1474
+ const active = resolveActiveProfile();
1475
+ if (json) {
1476
+ console.log(JSON.stringify({
1477
+ active,
1478
+ profiles: names
1479
+ }, null, 2));
1480
+ return;
1481
+ }
1482
+ if (names.length === 0) {
1483
+ logger.warn(`No profiles in ${PROFILES_DIR}`);
1484
+ logger.info("Run `gscdump auth login` to create one automatically, or `gscdump profile create <name>`");
1485
+ return;
1486
+ }
1487
+ for (const n of names) console.log(`${n === active ? "*" : " "} ${n}`);
1488
+ }
1489
+ });
1207
1490
  const profileCommand = defineCommand({
1208
1491
  meta: {
1209
1492
  name: "profile",
1210
1493
  description: "Manage gscdump profiles (per-account token + config dirs)"
1211
1494
  },
1212
1495
  subCommands: {
1213
- list: defineCommand({
1214
- meta: {
1215
- name: "list",
1216
- description: "List configured profiles"
1217
- },
1218
- args: { ...OUTPUT_ARGS },
1219
- async run({ args }) {
1220
- const { json } = applyOutputMode(args);
1221
- const names = await listProfiles();
1222
- const active = resolveActiveProfile();
1223
- if (json) {
1224
- console.log(JSON.stringify({
1225
- active,
1226
- profiles: names
1227
- }, null, 2));
1228
- return;
1229
- }
1230
- if (names.length === 0) {
1231
- logger.warn(`No profiles in ${PROFILES_DIR}`);
1232
- logger.info("Run `gscdump auth login` to create one automatically, or `gscdump profile create <name>`");
1233
- return;
1234
- }
1235
- for (const n of names) console.log(`${n === active ? "*" : " "} ${n}`);
1236
- }
1237
- }),
1496
+ list: listCmd,
1238
1497
  path: defineCommand({
1239
1498
  meta: {
1240
1499
  name: "path",
@@ -1375,8 +1634,31 @@ const profileCommand = defineCommand({
1375
1634
  logger.success(`Removed profile: ${name}`);
1376
1635
  }
1377
1636
  })
1637
+ },
1638
+ async run({ args }) {
1639
+ if (!noSubcommandSelected("profile", [
1640
+ "list",
1641
+ "path",
1642
+ "current",
1643
+ "use",
1644
+ "create",
1645
+ "clear",
1646
+ "delete"
1647
+ ])) return;
1648
+ await listCmd.run?.({
1649
+ args,
1650
+ cmd: listCmd,
1651
+ rawArgs: []
1652
+ });
1378
1653
  }
1379
1654
  });
1655
+ const AUTH_SUBCOMMANDS = [
1656
+ "status",
1657
+ "login",
1658
+ "logout",
1659
+ "refresh",
1660
+ "scopes"
1661
+ ];
1380
1662
  const REQUIRED_SCOPES$1 = [
1381
1663
  "https://www.googleapis.com/auth/webmasters",
1382
1664
  "https://www.googleapis.com/auth/indexing",
@@ -1405,6 +1687,66 @@ async function resolveLiveAuthState() {
1405
1687
  missing
1406
1688
  };
1407
1689
  }
1690
+ async function runStatus(args) {
1691
+ const { json } = applyOutputMode(args);
1692
+ const { byok, tokens, tokenInfo, scopes, missing } = await resolveLiveAuthState();
1693
+ const byokKind = byok ? typeof byok === "string" ? "access-token" : "refresh-token" : null;
1694
+ if (json) {
1695
+ console.log(JSON.stringify({
1696
+ authenticated: !!tokens || !!byok,
1697
+ source: byok ? "byok" : tokens ? "saved-tokens" : null,
1698
+ byokKind,
1699
+ scopes,
1700
+ tokenAccount: tokenInfo?.email ?? null,
1701
+ tokens: tokens ? {
1702
+ hasAccessToken: !!tokens.access_token,
1703
+ hasRefreshToken: !!tokens.refresh_token,
1704
+ expiry: tokens.expiry_date ? new Date(tokens.expiry_date).toISOString() : null,
1705
+ expired: tokens.expiry_date ? tokens.expiry_date < Date.now() : null
1706
+ } : null
1707
+ }, null, 2));
1708
+ return;
1709
+ }
1710
+ const reportScopes = () => {
1711
+ if (scopes.length === 0) return;
1712
+ console.log(` Scopes:`);
1713
+ for (const s of scopes) console.log(` \x1B[90m└─\x1B[0m ${s}`);
1714
+ if (missing.length > 0) {
1715
+ console.log(` \x1B[33mMissing scopes:\x1B[0m`);
1716
+ for (const s of missing) console.log(` \x1B[90m└─\x1B[0m ${s}`);
1717
+ console.log(` \x1B[90mRun \`gscdump auth login --force\` to re-consent.\x1B[0m`);
1718
+ }
1719
+ };
1720
+ if (byok) {
1721
+ logger.success(`Authenticated via BYOK (${byokKind})`);
1722
+ if (tokenInfo?.email) console.log(` Account: ${tokenInfo.email}`);
1723
+ reportScopes();
1724
+ return;
1725
+ }
1726
+ if (!tokens) {
1727
+ logger.warn("Not authenticated");
1728
+ logger.info("Run `gscdump init` (full setup) or `gscdump auth login` (OAuth only)");
1729
+ logger.info("Or set GSC_ACCESS_TOKEN / GSC_CLIENT_ID + GSC_CLIENT_SECRET + GSC_REFRESH_TOKEN env vars");
1730
+ return;
1731
+ }
1732
+ const hasAccess = !!tokens.access_token;
1733
+ const hasRefresh = !!tokens.refresh_token;
1734
+ const expiry = tokens.expiry_date ? new Date(tokens.expiry_date) : null;
1735
+ const isExpired = expiry && expiry < /* @__PURE__ */ new Date();
1736
+ if (isExpired && hasRefresh) logger.warn("Token expired; refresh available. Run `gscdump auth refresh`, or any live command will auto-refresh.");
1737
+ else if (isExpired) logger.error("Token expired and no refresh token present. Run `gscdump auth login`.");
1738
+ else logger.success("Authenticated (saved tokens)");
1739
+ console.log();
1740
+ console.log(` Access token: ${hasAccess ? "\x1B[32mpresent\x1B[0m" : "\x1B[31mmissing\x1B[0m"}`);
1741
+ console.log(` Refresh token: ${hasRefresh ? "\x1B[32mpresent\x1B[0m" : "\x1B[31mmissing\x1B[0m"}`);
1742
+ if (expiry) {
1743
+ const status = isExpired ? "\x1B[33mexpired\x1B[0m" : "\x1B[32mvalid\x1B[0m";
1744
+ console.log(` Expires: ${expiry.toISOString()} (${status})`);
1745
+ }
1746
+ if (tokenInfo?.email) console.log(` Account: ${tokenInfo.email}`);
1747
+ reportScopes();
1748
+ if (isExpired && !hasRefresh) process.exit(1);
1749
+ }
1408
1750
  const statusCommand$1 = defineCommand({
1409
1751
  meta: {
1410
1752
  name: "status",
@@ -1412,61 +1754,7 @@ const statusCommand$1 = defineCommand({
1412
1754
  },
1413
1755
  args: { ...OUTPUT_ARGS },
1414
1756
  async run({ args }) {
1415
- const { json } = applyOutputMode(args);
1416
- const { byok, tokens, tokenInfo, scopes, missing } = await resolveLiveAuthState();
1417
- const byokKind = byok ? typeof byok === "string" ? "access-token" : "refresh-token" : null;
1418
- if (json) {
1419
- console.log(JSON.stringify({
1420
- authenticated: !!tokens || !!byok,
1421
- source: byok ? "byok" : tokens ? "saved-tokens" : null,
1422
- byokKind,
1423
- scopes,
1424
- tokenAccount: tokenInfo?.email ?? null,
1425
- tokens: tokens ? {
1426
- hasAccessToken: !!tokens.access_token,
1427
- hasRefreshToken: !!tokens.refresh_token,
1428
- expiry: tokens.expiry_date ? new Date(tokens.expiry_date).toISOString() : null,
1429
- expired: tokens.expiry_date ? tokens.expiry_date < Date.now() : null
1430
- } : null
1431
- }, null, 2));
1432
- return;
1433
- }
1434
- const reportScopes = () => {
1435
- if (scopes.length === 0) return;
1436
- console.log(` Scopes:`);
1437
- for (const s of scopes) console.log(` \x1B[90m└─\x1B[0m ${s}`);
1438
- if (missing.length > 0) {
1439
- console.log(` \x1B[33mMissing scopes:\x1B[0m`);
1440
- for (const s of missing) console.log(` \x1B[90m└─\x1B[0m ${s}`);
1441
- console.log(` \x1B[90mRun \`gscdump auth login --force\` to re-consent.\x1B[0m`);
1442
- }
1443
- };
1444
- if (byok) {
1445
- logger.success(`Authenticated via BYOK (${byokKind})`);
1446
- if (tokenInfo?.email) console.log(` Account: ${tokenInfo.email}`);
1447
- reportScopes();
1448
- return;
1449
- }
1450
- if (!tokens) {
1451
- logger.warn("Not authenticated");
1452
- logger.info("Run `gscdump init` (full setup) or `gscdump auth login` (OAuth only)");
1453
- logger.info("Or set GSC_ACCESS_TOKEN / GSC_CLIENT_ID + GSC_CLIENT_SECRET + GSC_REFRESH_TOKEN env vars");
1454
- return;
1455
- }
1456
- const hasAccess = !!tokens.access_token;
1457
- const hasRefresh = !!tokens.refresh_token;
1458
- const expiry = tokens.expiry_date ? new Date(tokens.expiry_date) : null;
1459
- const isExpired = expiry && expiry < /* @__PURE__ */ new Date();
1460
- logger.success("Authenticated (saved tokens)");
1461
- console.log();
1462
- console.log(` Access token: ${hasAccess ? "\x1B[32mpresent\x1B[0m" : "\x1B[31mmissing\x1B[0m"}`);
1463
- console.log(` Refresh token: ${hasRefresh ? "\x1B[32mpresent\x1B[0m" : "\x1B[31mmissing\x1B[0m"}`);
1464
- if (expiry) {
1465
- const status = isExpired ? "\x1B[33mexpired\x1B[0m" : "\x1B[32mvalid\x1B[0m";
1466
- console.log(` Expires: ${expiry.toISOString()} (${status})`);
1467
- }
1468
- if (tokenInfo?.email) console.log(` Account: ${tokenInfo.email}`);
1469
- reportScopes();
1757
+ await runStatus(args);
1470
1758
  }
1471
1759
  });
1472
1760
  const refreshCommand = defineCommand({
@@ -1501,126 +1789,135 @@ const refreshCommand = defineCommand({
1501
1789
  else logger.warn("Refresh completed but no new access token returned");
1502
1790
  }
1503
1791
  });
1504
- const authCommand = defineCommand({
1792
+ const loginCommand = defineCommand({
1505
1793
  meta: {
1506
- name: "auth",
1507
- description: "Manage authentication"
1794
+ name: "login",
1795
+ description: "Run OAuth flow and persist tokens (skip if BYOK env vars set)"
1508
1796
  },
1509
- subCommands: {
1510
- status: statusCommand$1,
1511
- login: defineCommand({
1512
- meta: {
1513
- name: "login",
1514
- description: "Run OAuth flow and persist tokens (skip if BYOK env vars set)"
1515
- },
1516
- args: {
1517
- ...OUTPUT_ARGS,
1518
- "force": {
1519
- type: "boolean",
1520
- alias: "f",
1521
- default: false,
1522
- description: "Re-run OAuth even if tokens already exist"
1523
- },
1524
- "browser": {
1525
- type: "boolean",
1526
- default: true,
1527
- description: "Use loopback browser flow. Pass --no-browser for device-code (headless)."
1528
- },
1529
- "service-account": {
1530
- type: "string",
1531
- description: "Path to a service-account JSON key (skips OAuth)"
1532
- }
1533
- },
1534
- async run({ args }) {
1535
- applyOutputMode(args);
1536
- if (resolveBYOK() && !args.force) {
1537
- logger.info("BYOK env vars detected, no login needed (--force to override)");
1538
- return;
1539
- }
1540
- if (args["service-account"]) {
1541
- const saPath = path.resolve(String(args["service-account"]));
1542
- const jwt = await loadServiceAccount(saPath).catch((e) => {
1543
- logger.error(`Service-account load failed: ${e.message}`);
1544
- process.exit(1);
1545
- });
1546
- await jwt.authorize().catch((e) => {
1547
- logger.error(`Service-account auth failed: ${e.message}`);
1548
- process.exit(1);
1549
- });
1550
- const config = await loadConfig();
1551
- config.serviceAccountPath = saPath;
1552
- await saveConfig(config);
1553
- logger.success(`Service-account verified: ${jwt.email ?? "OK"}`);
1554
- logger.info(`Saved path to config: ${saPath}`);
1555
- return;
1556
- }
1557
- if (args.force) await clearTokens();
1558
- await getAuth({
1559
- interactive: true,
1560
- noBrowser: args.browser === false,
1561
- force: Boolean(args.force)
1562
- }).catch((e) => {
1563
- logger.error(`Login failed: ${e.message}`);
1564
- process.exit(1);
1565
- });
1566
- logger.success("Logged in");
1567
- if (!resolveActiveProfile()) {
1568
- const tokens = await loadTokens();
1569
- const info = tokens?.access_token ? await fetchTokenInfo(tokens.access_token) : null;
1570
- if (info?.email) {
1571
- const name = profileNameFromEmail(info.email);
1572
- if (await adoptCurrentConfigAsProfile(name).catch(() => null)) logger.success(`Saved as profile "${name}" (active)`);
1573
- }
1574
- }
1575
- if (resolveBYOK()) {
1576
- console.log();
1577
- console.log(await formatAuthProvenance());
1578
- }
1579
- }
1580
- }),
1581
- logout: defineCommand({
1582
- meta: {
1583
- name: "logout",
1584
- description: "Clear stored OAuth tokens"
1585
- },
1586
- args: { ...OUTPUT_ARGS },
1587
- async run({ args }) {
1588
- applyOutputMode(args);
1589
- await clearTokens();
1590
- const config = await loadConfig();
1591
- if (config.serviceAccountPath) {
1592
- delete config.serviceAccountPath;
1593
- await saveConfig(config);
1594
- logger.info("Cleared saved service-account path");
1595
- }
1797
+ args: {
1798
+ ...OUTPUT_ARGS,
1799
+ "force": {
1800
+ type: "boolean",
1801
+ alias: "f",
1802
+ default: false,
1803
+ description: "Re-run OAuth even if tokens already exist"
1804
+ },
1805
+ "browser": {
1806
+ type: "boolean",
1807
+ default: true,
1808
+ description: "Use loopback browser flow. Pass --no-browser for device-code (headless)."
1809
+ },
1810
+ "service-account": {
1811
+ type: "string",
1812
+ description: "Path to a service-account JSON key (skips OAuth)"
1813
+ }
1814
+ },
1815
+ async run({ args }) {
1816
+ applyOutputMode(args);
1817
+ if (resolveBYOK() && !args.force) {
1818
+ logger.info("BYOK env vars detected, no login needed (--force to override)");
1819
+ return;
1820
+ }
1821
+ if (args["service-account"]) {
1822
+ const saPath = path.resolve(String(args["service-account"]));
1823
+ const jwt = await loadServiceAccount(saPath).catch((e) => {
1824
+ logger.error(`Service-account load failed: ${e.message}`);
1825
+ process.exit(1);
1826
+ });
1827
+ await jwt.authorize().catch((e) => {
1828
+ logger.error(`Service-account auth failed: ${e.message}`);
1829
+ process.exit(1);
1830
+ });
1831
+ const config = await loadConfig();
1832
+ config.serviceAccountPath = saPath;
1833
+ await saveConfig(config);
1834
+ logger.success(`Service-account verified: ${jwt.email ?? "OK"}`);
1835
+ logger.info(`Saved path to config: ${saPath}`);
1836
+ return;
1837
+ }
1838
+ if (args.force) await clearTokens();
1839
+ const oauth = await getAuth({
1840
+ interactive: true,
1841
+ noBrowser: args.browser === false,
1842
+ force: Boolean(args.force)
1843
+ }).catch((e) => {
1844
+ logger.error(`Login failed: ${e.message}`);
1845
+ process.exit(1);
1846
+ });
1847
+ logger.success("Logged in");
1848
+ await runSmokeTest(oauth).catch(() => {});
1849
+ if (!resolveActiveProfile()) {
1850
+ const tokens = await loadTokens();
1851
+ const info = tokens?.access_token ? await fetchTokenInfo(tokens.access_token) : null;
1852
+ if (info?.email) {
1853
+ const name = profileNameFromEmail(info.email);
1854
+ if (await adoptCurrentConfigAsProfile(name).catch(() => null)) logger.success(`Saved as profile "${name}" (active)`);
1596
1855
  }
1597
- }),
1856
+ }
1857
+ if (resolveBYOK()) {
1858
+ console.log();
1859
+ console.log(await formatAuthProvenance());
1860
+ }
1861
+ }
1862
+ });
1863
+ const logoutCommand = defineCommand({
1864
+ meta: {
1865
+ name: "logout",
1866
+ description: "Clear stored OAuth tokens"
1867
+ },
1868
+ args: { ...OUTPUT_ARGS },
1869
+ async run({ args }) {
1870
+ applyOutputMode(args);
1871
+ await clearTokens();
1872
+ const config = await loadConfig();
1873
+ if (config.serviceAccountPath) {
1874
+ delete config.serviceAccountPath;
1875
+ await saveConfig(config);
1876
+ logger.info("Cleared saved service-account path");
1877
+ }
1878
+ }
1879
+ });
1880
+ const scopesCommand = defineCommand({
1881
+ meta: {
1882
+ name: "scopes",
1883
+ description: "Print granted OAuth scopes (one per line); exits 1 if any required scope is missing"
1884
+ },
1885
+ args: { ...OUTPUT_ARGS },
1886
+ async run({ args }) {
1887
+ const { json } = applyOutputMode(args);
1888
+ const { liveToken, scopes, missing } = await resolveLiveAuthState();
1889
+ if (!liveToken) {
1890
+ if (json) console.log(JSON.stringify({
1891
+ scopes: [],
1892
+ missing: null
1893
+ }, null, 2));
1894
+ else logger.error("Not authenticated");
1895
+ process.exit(1);
1896
+ }
1897
+ if (json) console.log(JSON.stringify({
1898
+ scopes,
1899
+ missing
1900
+ }, null, 2));
1901
+ else for (const s of scopes) console.log(s);
1902
+ if (missing.length > 0) process.exit(1);
1903
+ }
1904
+ });
1905
+ const authCommand = defineCommand({
1906
+ meta: {
1907
+ name: "auth",
1908
+ description: "Manage authentication"
1909
+ },
1910
+ args: { ...OUTPUT_ARGS },
1911
+ subCommands: {
1912
+ status: statusCommand$1,
1913
+ login: loginCommand,
1914
+ logout: logoutCommand,
1598
1915
  refresh: refreshCommand,
1599
- scopes: defineCommand({
1600
- meta: {
1601
- name: "scopes",
1602
- description: "Print granted OAuth scopes (one per line); exits 1 if any required scope is missing"
1603
- },
1604
- args: { ...OUTPUT_ARGS },
1605
- async run({ args }) {
1606
- const { json } = applyOutputMode(args);
1607
- const { liveToken, scopes, missing } = await resolveLiveAuthState();
1608
- if (!liveToken) {
1609
- if (json) console.log(JSON.stringify({
1610
- scopes: [],
1611
- missing: null
1612
- }, null, 2));
1613
- else logger.error("Not authenticated");
1614
- process.exit(1);
1615
- }
1616
- if (json) console.log(JSON.stringify({
1617
- scopes,
1618
- missing
1619
- }, null, 2));
1620
- else for (const s of scopes) console.log(s);
1621
- if (missing.length > 0) process.exit(1);
1622
- }
1623
- })
1916
+ scopes: scopesCommand
1917
+ },
1918
+ async run({ args }) {
1919
+ if (!noSubcommandSelected("auth", AUTH_SUBCOMMANDS)) return;
1920
+ await runStatus(args);
1624
1921
  }
1625
1922
  });
1626
1923
  const showCommand = defineCommand({
@@ -1747,7 +2044,7 @@ const configCommand = defineCommand({
1747
2044
  args: { ...OUTPUT_ARGS },
1748
2045
  async run({ args }) {
1749
2046
  const { json } = applyOutputMode(args);
1750
- const { resolveDataDir } = await import("./_chunks/config.mjs").then((n) => n.t);
2047
+ const { resolveDataDir } = await Promise.resolve().then(() => config_exports);
1751
2048
  const fs = await import("node:fs/promises");
1752
2049
  const config = await loadConfig();
1753
2050
  const issues = [];
@@ -1834,6 +2131,20 @@ const configCommand = defineCommand({
1834
2131
  if (issues.some((i) => i.level === "fail")) process.exit(1);
1835
2132
  }
1836
2133
  })
2134
+ },
2135
+ async run({ args }) {
2136
+ if (!noSubcommandSelected("config", [
2137
+ "show",
2138
+ "set",
2139
+ "unset",
2140
+ "path",
2141
+ "validate"
2142
+ ])) return;
2143
+ await showCommand.run?.({
2144
+ args,
2145
+ cmd: showCommand,
2146
+ rawArgs: []
2147
+ });
1837
2148
  }
1838
2149
  });
1839
2150
  const REQUIRED_SCOPES = [
@@ -2352,7 +2663,7 @@ async function dumpParquet(store, entries, outDir) {
2352
2663
  const bytes = await store.engine.readObject(entry.objectKey);
2353
2664
  const target = path.join(outDir, entry.objectKey);
2354
2665
  await fs.mkdir(path.dirname(target), { recursive: true });
2355
- await fs.writeFile(target, Buffer.from(bytes));
2666
+ await fs.writeFile(target, Buffer$1.from(bytes));
2356
2667
  copied++;
2357
2668
  }
2358
2669
  return copied;
@@ -2414,7 +2725,7 @@ async function readUrlList(opts) {
2414
2725
  if (opts.file) return (await readFile(opts.file, "utf8")).split("\n").map((l) => l.trim()).filter(Boolean);
2415
2726
  const chunks = [];
2416
2727
  for await (const chunk of process.stdin) chunks.push(chunk);
2417
- return Buffer.concat(chunks).toString("utf8").split("\n").map((l) => l.trim()).filter(Boolean);
2728
+ return Buffer$1.concat(chunks).toString("utf8").split("\n").map((l) => l.trim()).filter(Boolean);
2418
2729
  }
2419
2730
  const inspectSubCommand = defineCommand({
2420
2731
  meta: {
@@ -3106,157 +3417,6 @@ const indexingCommand = defineCommand({
3106
3417
  "quota": quotaCommand
3107
3418
  }
3108
3419
  });
3109
- const ENV_LINE_RE = /^([^=]+)=(.*)$/;
3110
- async function promptDataDir(existing) {
3111
- const fallback = existing ?? defaultDataDir();
3112
- const answer = await text({
3113
- message: "Where should Parquet data be stored?",
3114
- placeholder: fallback,
3115
- defaultValue: fallback
3116
- });
3117
- if (isCancel(answer)) process.exit(1);
3118
- return String(answer) || fallback;
3119
- }
3120
- async function loadEnvFile() {
3121
- const envPath = path.join(process.cwd(), ".env");
3122
- const content = await fs.readFile(envPath, "utf-8").catch(() => null);
3123
- if (!content) return null;
3124
- const env = {};
3125
- for (const line of content.split("\n")) {
3126
- const trimmed = line.trim();
3127
- if (!trimmed || trimmed.startsWith("#")) continue;
3128
- const match = trimmed.match(ENV_LINE_RE);
3129
- if (match) {
3130
- const key = match[1].trim();
3131
- let value = match[2].trim();
3132
- if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
3133
- env[key] = value;
3134
- }
3135
- }
3136
- return env;
3137
- }
3138
- const initCommand = defineCommand({
3139
- meta: {
3140
- name: "init",
3141
- description: "Set up GSCDump authentication"
3142
- },
3143
- args: {
3144
- "force": {
3145
- type: "boolean",
3146
- alias: "f",
3147
- description: "Force re-initialization"
3148
- },
3149
- "no-store": {
3150
- type: "boolean",
3151
- default: false,
3152
- description: "Skip dataDir prompt (auth-only setup)"
3153
- },
3154
- ...OUTPUT_ARGS
3155
- },
3156
- async run({ args }) {
3157
- applyOutputMode(args);
3158
- const config = await loadConfig();
3159
- if (config.clientId && config.clientSecret && !args.force) {
3160
- logger.info("Already configured");
3161
- logger.info("Run with --force to reconfigure");
3162
- return;
3163
- }
3164
- const byok = resolveBYOK();
3165
- if (byok) {
3166
- const dataDir = args["no-store"] ? void 0 : await promptDataDir(config.dataDir);
3167
- await saveConfig({
3168
- ...config,
3169
- dataDir: dataDir ?? config.dataDir
3170
- });
3171
- logger.success(`BYOK detected (${typeof byok === "string" ? "access-token" : "refresh-token"}) — auth setup skipped`);
3172
- logger.success("Setup complete! Run gscdump to get started.");
3173
- return;
3174
- }
3175
- const envFile = await loadEnvFile();
3176
- const envCid = envFile?.GSC_CLIENT_ID ?? envFile?.GOOGLE_CLIENT_ID;
3177
- const envSec = envFile?.GSC_CLIENT_SECRET ?? envFile?.GOOGLE_CLIENT_SECRET;
3178
- const envRef = envFile?.GSC_REFRESH_TOKEN ?? envFile?.GOOGLE_REFRESH_TOKEN;
3179
- const envAcc = envFile?.GSC_ACCESS_TOKEN ?? envFile?.GOOGLE_ACCESS_TOKEN;
3180
- if (envCid && envSec && envRef) {
3181
- logger.info("Found .env file with credentials");
3182
- process.env.GOOGLE_CLIENT_ID = envCid;
3183
- process.env.GOOGLE_CLIENT_SECRET = envSec;
3184
- process.env.GOOGLE_REFRESH_TOKEN = envRef;
3185
- if (envAcc) process.env.GOOGLE_ACCESS_TOKEN = envAcc;
3186
- await saveConfig({
3187
- ...config,
3188
- clientId: envCid,
3189
- clientSecret: envSec,
3190
- dataDir: config.dataDir ?? defaultDataDir()
3191
- });
3192
- const creds = (await authenticate({
3193
- clientId: envCid,
3194
- clientSecret: envSec
3195
- }, false)).credentials;
3196
- if (creds.access_token) await saveTokens({
3197
- access_token: creds.access_token,
3198
- refresh_token: creds.refresh_token || envRef,
3199
- expiry_date: creds.expiry_date
3200
- });
3201
- console.log();
3202
- logger.success("Setup complete using .env credentials! Run gscdump to get started.");
3203
- return;
3204
- }
3205
- console.log();
3206
- console.log(" \x1B[1mWelcome to GSCDump!\x1B[0m");
3207
- console.log(" \x1B[90mGoogle Search Console data extraction CLI\x1B[0m");
3208
- console.log();
3209
- const dataDir = args["no-store"] ? void 0 : await promptDataDir(config.dataDir);
3210
- const credentials = await getAuthCredentials(true);
3211
- await saveConfig({
3212
- ...config,
3213
- ...dataDir ? { dataDir } : {},
3214
- clientId: credentials.clientId,
3215
- clientSecret: credentials.clientSecret
3216
- });
3217
- await smokeTest(await authenticate(credentials, true));
3218
- await maybeWriteEnvFile(credentials.clientId, credentials.clientSecret);
3219
- console.log();
3220
- logger.success("Setup complete! Run gscdump to get started.");
3221
- }
3222
- });
3223
- async function smokeTest(oauth) {
3224
- const sites = await googleSearchConsole(oauth).sites().catch((e) => e);
3225
- if (sites instanceof Error) {
3226
- logger.warn(`Smoke test failed: ${sites.message}`);
3227
- logger.info("Auth saved, but verify scopes via `gscdump auth status` / `gscdump doctor`.");
3228
- return;
3229
- }
3230
- logger.success(`Verified: ${sites.length} GSC site(s) accessible`);
3231
- }
3232
- async function maybeWriteEnvFile(clientId, clientSecret) {
3233
- const tokens = await loadTokens();
3234
- if (!tokens?.refresh_token) return;
3235
- const wants = await confirm({
3236
- message: "Write a `.env` file with these credentials? (handy for CI / other machines)",
3237
- initialValue: false
3238
- });
3239
- if (isCancel(wants) || !wants) return;
3240
- const envPath = path.join(process.cwd(), ".env");
3241
- if (await fs.stat(envPath).then(() => true).catch(() => false)) {
3242
- const overwrite = await confirm({
3243
- message: `${displayPath(envPath)} exists. Overwrite?`,
3244
- initialValue: false
3245
- });
3246
- if (isCancel(overwrite) || !overwrite) {
3247
- logger.info(`Skipped — keep credentials at ${displayPath(envPath)} manually if needed`);
3248
- return;
3249
- }
3250
- }
3251
- const content = [
3252
- `GSC_CLIENT_ID=${clientId}`,
3253
- `GSC_CLIENT_SECRET=${clientSecret}`,
3254
- `GSC_REFRESH_TOKEN=${tokens.refresh_token}`,
3255
- ""
3256
- ].join("\n");
3257
- await fs.writeFile(envPath, content, { mode: 384 });
3258
- logger.success(`Wrote ${displayPath(envPath)}`);
3259
- }
3260
3420
  function verdictTone(verdict) {
3261
3421
  if (verdict === "PASS") return "\x1B[32m";
3262
3422
  if (verdict === "NEUTRAL" || verdict === "PARTIAL") return "\x1B[33m";
@@ -3505,6 +3665,7 @@ const DIMENSIONS = [
3505
3665
  "page",
3506
3666
  "query",
3507
3667
  "date",
3668
+ "hour",
3508
3669
  "country",
3509
3670
  "device",
3510
3671
  "searchAppearance"
@@ -3513,6 +3674,7 @@ const DIM_COLUMNS = {
3513
3674
  page,
3514
3675
  query,
3515
3676
  date,
3677
+ hour,
3516
3678
  country,
3517
3679
  device,
3518
3680
  searchAppearance
@@ -3570,7 +3732,7 @@ async function runLiveQuery(client, siteUrl, opts) {
3570
3732
  dimensions: opts.dimensions,
3571
3733
  rowLimit: opts.rowLimit
3572
3734
  };
3573
- if (opts.searchType) baseBody.searchType = opts.searchType;
3735
+ if (opts.searchType) baseBody.type = opts.searchType;
3574
3736
  if (opts.dataState) baseBody.dataState = opts.dataState;
3575
3737
  if (opts.aggregationType) baseBody.aggregationType = opts.aggregationType;
3576
3738
  if (opts.dimensionFilter) {
@@ -3756,7 +3918,7 @@ const queryCommand = defineCommand({
3756
3918
  dimensions: dimNames,
3757
3919
  rowLimit
3758
3920
  };
3759
- if (searchType) body.searchType = searchType;
3921
+ if (searchType) body.type = searchType;
3760
3922
  if (dataState) body.dataState = dataState;
3761
3923
  if (aggregationType) body.aggregationType = aggregationType;
3762
3924
  if (dimensionFilter) body.dimensionFilterGroups = filterToGroups(dimensionFilter);
@@ -4215,69 +4377,70 @@ const reportCommand = defineCommand({
4215
4377
  ...Object.fromEntries(REPORT_IDS.map((id) => [id, makeReportCommand(defaultReportRegistry.getReport(id))]))
4216
4378
  }
4217
4379
  });
4380
+ const listCommand$1 = defineCommand({
4381
+ meta: {
4382
+ name: "list",
4383
+ description: "List sitemaps for a site"
4384
+ },
4385
+ args: {
4386
+ ...OUTPUT_ARGS,
4387
+ site: {
4388
+ type: "string",
4389
+ alias: "s",
4390
+ description: "Site URL (e.g., sc-domain:example.com or https://example.com/)"
4391
+ },
4392
+ pending: {
4393
+ type: "boolean",
4394
+ default: false,
4395
+ description: "Show only sitemaps with isPending=true"
4396
+ },
4397
+ errored: {
4398
+ type: "boolean",
4399
+ default: false,
4400
+ description: "Show only sitemaps with errors > 0"
4401
+ }
4402
+ },
4403
+ async run({ args }) {
4404
+ const { json } = applyOutputMode(args);
4405
+ const ctx = await createCommandContext({ needsAuth: true });
4406
+ const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
4407
+ let sitemaps = (await ctx.client.sitemaps.list(siteUrl).catch(gscErrorHandler)).map((sm) => ({
4408
+ path: sm.path,
4409
+ type: sm.type || void 0,
4410
+ isPending: sm.isPending || false,
4411
+ errors: Number(sm.errors) || 0,
4412
+ warnings: Number(sm.warnings) || 0,
4413
+ lastDownloaded: sm.lastDownloaded || null,
4414
+ lastSubmitted: sm.lastSubmitted || null
4415
+ }));
4416
+ if (args.pending) sitemaps = sitemaps.filter((sm) => sm.isPending);
4417
+ if (args.errored) sitemaps = sitemaps.filter((sm) => sm.errors > 0);
4418
+ if (json) {
4419
+ console.log(JSON.stringify(sitemaps, null, 2));
4420
+ return;
4421
+ }
4422
+ if (sitemaps.length === 0) {
4423
+ logger.warn("No sitemaps found");
4424
+ return;
4425
+ }
4426
+ logger.success(`Found ${sitemaps.length} sitemaps:`);
4427
+ console.log();
4428
+ for (const sm of sitemaps) {
4429
+ const pending = sm.isPending ? " \x1B[33m(pending)\x1B[0m" : "";
4430
+ const errors = sm.errors ? ` \x1B[31m${sm.errors} errors\x1B[0m` : "";
4431
+ const warnings = sm.warnings ? ` \x1B[33m${sm.warnings} warnings\x1B[0m` : "";
4432
+ const submitted = sm.lastSubmitted ? ` \x1B[90msubmitted ${sm.lastSubmitted}\x1B[0m` : "";
4433
+ console.log(` ${sm.path}${pending}${errors}${warnings}${submitted}`);
4434
+ }
4435
+ }
4436
+ });
4218
4437
  const sitemapsCommand = defineCommand({
4219
4438
  meta: {
4220
4439
  name: "sitemaps",
4221
4440
  description: "Manage sitemaps"
4222
4441
  },
4223
4442
  subCommands: {
4224
- list: defineCommand({
4225
- meta: {
4226
- name: "list",
4227
- description: "List sitemaps for a site"
4228
- },
4229
- args: {
4230
- ...OUTPUT_ARGS,
4231
- site: {
4232
- type: "string",
4233
- alias: "s",
4234
- description: "Site URL (e.g., sc-domain:example.com or https://example.com/)"
4235
- },
4236
- pending: {
4237
- type: "boolean",
4238
- default: false,
4239
- description: "Show only sitemaps with isPending=true"
4240
- },
4241
- errored: {
4242
- type: "boolean",
4243
- default: false,
4244
- description: "Show only sitemaps with errors > 0"
4245
- }
4246
- },
4247
- async run({ args }) {
4248
- const { json } = applyOutputMode(args);
4249
- const ctx = await createCommandContext({ needsAuth: true });
4250
- const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
4251
- let sitemaps = (await ctx.client.sitemaps.list(siteUrl).catch(gscErrorHandler)).map((sm) => ({
4252
- path: sm.path,
4253
- type: sm.type || void 0,
4254
- isPending: sm.isPending || false,
4255
- errors: Number(sm.errors) || 0,
4256
- warnings: Number(sm.warnings) || 0,
4257
- lastDownloaded: sm.lastDownloaded || null,
4258
- lastSubmitted: sm.lastSubmitted || null
4259
- }));
4260
- if (args.pending) sitemaps = sitemaps.filter((sm) => sm.isPending);
4261
- if (args.errored) sitemaps = sitemaps.filter((sm) => sm.errors > 0);
4262
- if (json) {
4263
- console.log(JSON.stringify(sitemaps, null, 2));
4264
- return;
4265
- }
4266
- if (sitemaps.length === 0) {
4267
- logger.warn("No sitemaps found");
4268
- return;
4269
- }
4270
- logger.success(`Found ${sitemaps.length} sitemaps:`);
4271
- console.log();
4272
- for (const sm of sitemaps) {
4273
- const pending = sm.isPending ? " \x1B[33m(pending)\x1B[0m" : "";
4274
- const errors = sm.errors ? ` \x1B[31m${sm.errors} errors\x1B[0m` : "";
4275
- const warnings = sm.warnings ? ` \x1B[33m${sm.warnings} warnings\x1B[0m` : "";
4276
- const submitted = sm.lastSubmitted ? ` \x1B[90msubmitted ${sm.lastSubmitted}\x1B[0m` : "";
4277
- console.log(` ${sm.path}${pending}${errors}${warnings}${submitted}`);
4278
- }
4279
- }
4280
- }),
4443
+ list: listCommand$1,
4281
4444
  get: defineCommand({
4282
4445
  meta: {
4283
4446
  name: "get",
@@ -4464,6 +4627,21 @@ const sitemapsCommand = defineCommand({
4464
4627
  for (const u of urls) console.log(u);
4465
4628
  }
4466
4629
  })
4630
+ },
4631
+ async run({ args }) {
4632
+ if (!noSubcommandSelected("sitemaps", [
4633
+ "list",
4634
+ "get",
4635
+ "submit",
4636
+ "delete",
4637
+ "discover",
4638
+ "urls"
4639
+ ])) return;
4640
+ await listCommand$1.run?.({
4641
+ args,
4642
+ cmd: listCommand$1,
4643
+ rawArgs: []
4644
+ });
4467
4645
  }
4468
4646
  });
4469
4647
  const ALL_METHODS = [
@@ -5540,6 +5718,22 @@ const storeCommand = defineCommand({
5540
5718
  console.log(` Sync states: ${result.syncStatesRemoved}`);
5541
5719
  }
5542
5720
  })
5721
+ },
5722
+ async run({ args }) {
5723
+ if (!noSubcommandSelected("store", [
5724
+ "stats",
5725
+ "compact",
5726
+ "gc",
5727
+ "export",
5728
+ "rollups",
5729
+ "rm-site",
5730
+ "reset"
5731
+ ])) return;
5732
+ await statsCommand.run?.({
5733
+ args,
5734
+ cmd: statsCommand,
5735
+ rawArgs: []
5736
+ });
5543
5737
  }
5544
5738
  });
5545
5739
  const DEFAULT_TABLES = [
@@ -6070,6 +6264,9 @@ runMain(defineCommand({
6070
6264
  analyze: analyzeCommand,
6071
6265
  report: reportCommand,
6072
6266
  auth: authCommand,
6267
+ login: loginCommand,
6268
+ logout: logoutCommand,
6269
+ status: statusCommand$1,
6073
6270
  config: configCommand,
6074
6271
  profile: profileCommand,
6075
6272
  doctor: doctorCommand,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/cli",
3
3
  "type": "module",
4
- "version": "0.17.5",
4
+ "version": "0.18.1",
5
5
  "description": "CLI for Google Search Console - dump, query, and run MCP server",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -42,11 +42,11 @@
42
42
  "google-auth-library": "^10.6.2",
43
43
  "ofetch": "^1.5.1",
44
44
  "open": "^11.0.0",
45
- "@gscdump/analysis": "0.17.5",
46
- "@gscdump/mcp": "0.17.5",
47
- "gscdump": "0.17.5",
48
- "@gscdump/engine": "0.17.5",
49
- "@gscdump/engine-gsc-api": "0.17.5"
45
+ "@gscdump/engine": "0.18.1",
46
+ "@gscdump/analysis": "0.18.1",
47
+ "@gscdump/engine-gsc-api": "0.18.1",
48
+ "@gscdump/mcp": "0.18.1",
49
+ "gscdump": "0.18.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@duckdb/node-api": "1.5.1-r.2",
@@ -1,54 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import os from "node:os";
4
- var __defProp = Object.defineProperty;
5
- var __exportAll = (all, no_symbols) => {
6
- let target = {};
7
- for (var name in all) __defProp(target, name, {
8
- get: all[name],
9
- enumerable: true
10
- });
11
- if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
12
- return target;
13
- };
14
- var config_exports = /* @__PURE__ */ __exportAll({
15
- defaultDataDir: () => defaultDataDir,
16
- getConfigDir: () => getConfigDir,
17
- getConfigPath: () => getConfigPath,
18
- loadConfig: () => loadConfig,
19
- resolveDataDir: () => resolveDataDir,
20
- saveConfig: () => saveConfig,
21
- setConfigDir: () => setConfigDir
22
- });
23
- let configDir = path.join(os.homedir(), ".config", "gscdump");
24
- function setConfigDir(dir) {
25
- configDir = dir;
26
- }
27
- function getConfigDir() {
28
- return configDir;
29
- }
30
- function defaultDataDir() {
31
- return path.join(os.homedir(), ".gscdump", "data");
32
- }
33
- function resolveDataDir(config) {
34
- return expandTilde(config.dataDir ?? defaultDataDir());
35
- }
36
- function expandTilde(p) {
37
- if (p === "~") return os.homedir();
38
- if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
39
- return p;
40
- }
41
- async function loadConfig() {
42
- return fs.readFile(path.join(configDir, "config.json"), "utf-8").then((data) => JSON.parse(data)).catch(() => ({}));
43
- }
44
- async function saveConfig(config) {
45
- await fs.mkdir(configDir, {
46
- recursive: true,
47
- mode: 448
48
- });
49
- await fs.writeFile(path.join(configDir, "config.json"), JSON.stringify(config, null, 2), { mode: 384 });
50
- }
51
- function getConfigPath() {
52
- return path.join(configDir, "config.json");
53
- }
54
- export { loadConfig as a, setConfigDir as c, getConfigPath as i, __exportAll as l, defaultDataDir as n, resolveDataDir as o, getConfigDir as r, saveConfig as s, config_exports as t };