@gscdump/cli 4.8.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import { LocalStoreUnsupportedError } from "./error-handler.mjs";
2
1
  import { useCliRuntime } from "./runtime.mjs";
2
+ import { LocalStoreUnsupportedError } from "./error-handler.mjs";
3
3
  import { logger } from "./utils.mjs";
4
4
  import { inferTable } from "./local-store.mjs";
5
5
  import { createCommandContext } from "./context.mjs";
@@ -174,7 +174,7 @@ async function resolveAnalysisSource(args) {
174
174
  auth
175
175
  });
176
176
  const route = decideRoute(req, state);
177
- if (route.kind === "syncing" || route.kind === "prompt") stopAtRoute(route, req, state.auth, { json: Boolean(args.json) });
177
+ if (route.kind === "syncing" || route.kind === "prompt") stopAtRoute(route, req, state.auth);
178
178
  if (route.kind === "local") {
179
179
  const source = createEngineQuerySource({
180
180
  engine: store.engine,
@@ -1,15 +1,23 @@
1
- import { HOSTED_KEY_REJECTED } from "./error-handler.mjs";
2
1
  import { useCliRuntime } from "./runtime.mjs";
2
+ import { stopError } from "./stop.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { z } from "zod";
7
7
  const HOSTED_SESSION_REJECTED = "gscdump.com rejected the CLI session. Run `gscdump auth login --mode hosted` again.";
8
+ const HOSTED_KEY_REJECTED = "gscdump.com rejected the API key. Run `gscdump auth login --mode hosted --api-key KEY` with a valid key.";
8
9
  const LOCAL_MODE_REQUIRED = [
9
10
  "This command calls Google, so it needs Local mode.",
10
11
  "Hosted mode reads your gscdump.com record and never calls Google.",
11
12
  "If you want Local mode, run `gscdump auth login --mode local`."
12
13
  ].join("\n");
14
+ function localModeRequired(detail) {
15
+ return stopError({
16
+ code: "LOCAL_MODE_REQUIRED",
17
+ message: detail ? `${detail}\n${LOCAL_MODE_REQUIRED}` : LOCAL_MODE_REQUIRED,
18
+ nextCommand: "gscdump auth login --mode local"
19
+ });
20
+ }
13
21
  const HOSTED_NOT_SET_UP = "Hosted credentials are missing. Run `gscdump auth login --mode hosted`, or set GSCDUMP_API_KEY.";
14
22
  const SAVED_STATE_INVALID = "The saved access mode is invalid. Run `gscdump auth login --mode local` or `gscdump auth login --mode hosted`.";
15
23
  const apiRootSchema = z.url().transform((value) => value.replace(/\/+$/, "")).refine((value) => {
@@ -129,7 +137,11 @@ async function resolveAuthentication() {
129
137
  if (env.GSCDUMP_API_ROOT && env.GSCDUMP_API_ROOT.replace(/\/+$/, "") !== state.apiRoot) throw new Error("The API root changed. Run `gscdump auth login --mode hosted` for the new API root.");
130
138
  return state;
131
139
  }
132
- throw new Error(HOSTED_NOT_SET_UP);
140
+ throw stopError({
141
+ code: "HOSTED_CREDENTIALS_MISSING",
142
+ message: HOSTED_NOT_SET_UP,
143
+ nextCommand: "gscdump auth login --mode hosted"
144
+ });
133
145
  }
134
146
  const hostedSiteSchema = z.object({
135
147
  siteId: z.string(),
@@ -162,18 +174,41 @@ async function hostedRequest(state, route, options = {}) {
162
174
  redirect: "error"
163
175
  });
164
176
  if (!response.ok) {
165
- const message = response.status === 401 ? "sessionId" in state ? HOSTED_SESSION_REJECTED : HOSTED_KEY_REJECTED : `Hosted request failed (${response.status}) for ${route.split("?")[0]}. Check \`gscdump auth status\`.`;
166
- throw Object.assign(new Error(message), {
177
+ const details = {
167
178
  statusCode: response.status,
168
179
  retryAfter: response.headers.get("retry-after"),
169
180
  response
170
- });
181
+ };
182
+ if (response.status === 401) {
183
+ const session = "sessionId" in state;
184
+ throw Object.assign(stopError({
185
+ code: "HOSTED_CREDENTIALS_REJECTED",
186
+ message: session ? HOSTED_SESSION_REJECTED : HOSTED_KEY_REJECTED,
187
+ nextCommand: session ? "gscdump auth login --mode hosted" : null
188
+ }), details);
189
+ }
190
+ throw Object.assign(/* @__PURE__ */ new Error(`Hosted request failed (${response.status}) for ${route.split("?")[0]}. Check \`gscdump auth status\`.`), details);
171
191
  }
172
192
  return response.status === 204 ? void 0 : response.json();
173
193
  }
174
194
  function hostedCredential(state) {
175
195
  return "sessionId" in state ? state.sessionId : state.apiKey;
176
196
  }
197
+ function siteManagerOf(state) {
198
+ return "sessionId" in state ? { _tag: "gscdump" } : { _tag: "key_issuer" };
199
+ }
200
+ function connectSiteStep(manager) {
201
+ switch (manager._tag) {
202
+ case "gscdump": return "Connect a Site at https://gscdump.com/app/onboarding?step=connect-sites.";
203
+ case "key_issuer": return "Connect a Site in the app that issued this API key.";
204
+ }
205
+ }
206
+ function siteSettingsPlace(manager) {
207
+ switch (manager._tag) {
208
+ case "gscdump": return "in the Site settings on gscdump.com";
209
+ case "key_issuer": return "in the Site settings of the app that issued this API key";
210
+ }
211
+ }
177
212
  async function getHostedAccount(state) {
178
213
  const result = accountSchema.safeParse(await hostedRequest(state, "/cli/me"));
179
214
  if (!result.success) throw new Error("gscdump.com returned invalid account data.");
@@ -183,4 +218,4 @@ function formatHostedSync(site) {
183
218
  const status = site.syncStatus ?? "pending";
184
219
  return `${status}${site.syncProgress && site.syncProgress.total > 0 && status !== "synced" ? `: ${site.syncProgress.completed.toLocaleString("en-US")} of ${site.syncProgress.total.toLocaleString("en-US")} days (${Math.round(site.syncProgress.percent)}%)` : ""}${status === "synced" && site.oldestDateSynced && site.newestDateSynced ? `: ${site.oldestDateSynced} to ${site.newestDateSynced}` : ""}`;
185
220
  }
186
- export { HOSTED_NOT_SET_UP, HOSTED_SESSION_REJECTED, LOCAL_MODE_REQUIRED, clearAuthentication, formatHostedSync, getHostedAccount, hostedCredential, hostedRequest, parseAuthMode, parseAuthentication, resolveAccessMode, resolveAuthentication, revokeHostedSession, saveAuthentication };
221
+ export { HOSTED_KEY_REJECTED, HOSTED_NOT_SET_UP, HOSTED_SESSION_REJECTED, LOCAL_MODE_REQUIRED, clearAuthentication, connectSiteStep, formatHostedSync, getHostedAccount, hostedCredential, hostedRequest, localModeRequired, parseAuthMode, parseAuthentication, resolveAccessMode, resolveAuthentication, revokeHostedSession, saveAuthentication, siteManagerOf, siteSettingsPlace };
package/dist/auth.mjs CHANGED
@@ -1,7 +1,6 @@
1
1
  import { resolveAuthentication } from "./auth-state.mjs";
2
2
  import { getConfigDir, loadConfig, saveConfig } from "./config.mjs";
3
3
  import { pickCliEnvironmentValue, resolveCliEnvironment } from "./environment.mjs";
4
- import { getAppliedEnvKeys, getLoadedEnvPath } from "./env-file.mjs";
5
4
  import { displayPath, logger } from "./utils.mjs";
6
5
  import process from "node:process";
7
6
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
@@ -361,7 +360,7 @@ async function resolveAuth(opts = {}) {
361
360
  return getAuth(opts);
362
361
  }
363
362
  function envSourceLabel(envVar) {
364
- return getAppliedEnvKeys().has(envVar) ? `.env (${envVar})` : `shell env (${envVar})`;
363
+ return `environment (${envVar})`;
365
364
  }
366
365
  function pickEnvSource(...envVars) {
367
366
  return pickCliEnvironmentValue(envVars);
@@ -451,11 +450,6 @@ async function describeAuthProvenance() {
451
450
  else if (tokens) effective = "saved-tokens";
452
451
  else effective = "none";
453
452
  if ((effective === "byok-refresh-token" || effective === "byok-access-token") && tokens) warnings.push("Environment credentials take priority over saved tokens. If you just ran `auth login --force`, unset or update the environment tokens.");
454
- if (effective === "byok-refresh-token" && clientId && refreshTok) {
455
- if (getAppliedEnvKeys().has(clientId.envVar) !== getAppliedEnvKeys().has(refreshTok.envVar)) warnings.push(`client_id and refresh_token come from different sources (${envSourceLabel(clientId.envVar)} vs ${envSourceLabel(refreshTok.envVar)}); they may not match.`);
456
- }
457
- const envFile = getLoadedEnvPath();
458
- if (envFile && getAppliedEnvKeys().size > 0) warnings.push(`Loaded ${getAppliedEnvKeys().size} var(s) from ${displayPath(envFile)}.`);
459
453
  return {
460
454
  rows,
461
455
  effective,
@@ -1,7 +1,10 @@
1
- import { getHostedAccount, hostedCredential } from "./auth-state.mjs";
1
+ import { stopError } from "./stop.mjs";
2
+ import { getHostedAccount, hostedCredential, siteManagerOf } from "./auth-state.mjs";
2
3
  import { logger } from "./utils.mjs";
3
4
  import { writeBingDump } from "./bing-data.mjs";
5
+ import { noSitesStop } from "./hosted-site.mjs";
4
6
  import { createGscdumpV1Client } from "@gscdump/sdk/v1";
7
+ const BING_SITES_COMMAND = "gscdump bing sites --json";
5
8
  function hostedBingClient(state) {
6
9
  return createGscdumpV1Client({
7
10
  apiRoot: state.apiRoot,
@@ -30,15 +33,33 @@ async function loadHostedBingConnections(state, sites, options = {}) {
30
33
  async function resolveHostedBingSites(state, input) {
31
34
  if (Boolean(input.site) === Boolean(input.allSites)) throw new Error("Choose --site or --all-sites.");
32
35
  const account = await getHostedAccount(state);
36
+ if (account.sites.length === 0) throw stopError(noSitesStop(siteManagerOf(state)));
33
37
  const exact = account.sites.filter((site) => site.siteId === input.site || site.siteUrl === input.site);
34
38
  const normalize = (value) => URL.parse(value.startsWith("sc-domain:") ? `https://${value.slice(10)}/` : value)?.toString();
35
39
  const requested = input.allSites ? account.sites : exact.length ? exact : account.sites.filter((site) => normalize(site.siteUrl) === normalize(input.site) && normalize(input.site) !== void 0);
36
- if (requested.length === 0) throw new Error("No matching Bing site. Run `gscdump bing sites`.");
37
- if (!input.allSites && requested.length > 1) throw new Error("Multiple Bing sites match. Use a site ID from `gscdump bing sites`.");
40
+ if (requested.length === 0) throw stopError({
41
+ code: "SITE_NOT_FOUND",
42
+ message: "No matching Bing site. Run `gscdump bing sites`.",
43
+ nextCommand: BING_SITES_COMMAND
44
+ });
45
+ if (!input.allSites && requested.length > 1) throw stopError({
46
+ code: "SITE_AMBIGUOUS",
47
+ message: "Multiple Bing sites match. Use a site ID from `gscdump bing sites`.",
48
+ nextCommand: BING_SITES_COMMAND
49
+ });
38
50
  const connections = await loadHostedBingConnections(state, requested, { tolerateFailures: Boolean(input.allSites) });
39
51
  const selected = input.allSites ? connections.filter((site) => site.connection._tag === "connected") : connections;
40
- if (selected.length === 0) throw new Error("No matching Bing site. Run `gscdump bing sites`.");
41
- if (input.requireConnected !== false && selected.some((site) => site.connection._tag !== "connected")) throw new Error("Bing is not connected. Run `gscdump bing login --site SITE_ID`.");
52
+ if (selected.length === 0) throw stopError({
53
+ code: "SITE_NOT_FOUND",
54
+ message: "No matching Bing site. Run `gscdump bing sites`.",
55
+ nextCommand: BING_SITES_COMMAND
56
+ });
57
+ const unconnected = input.requireConnected === false ? void 0 : selected.find((site) => site.connection._tag !== "connected");
58
+ if (unconnected) throw stopError({
59
+ code: "BING_NOT_CONNECTED",
60
+ message: `Bing is not connected for ${unconnected.siteUrl}. Run \`gscdump bing login --site ${unconnected.siteId}\`.`,
61
+ nextCommand: `gscdump bing login --site ${unconnected.siteId}`
62
+ });
42
63
  return selected;
43
64
  }
44
65
  async function dumpHostedBingSite(state, site, outDir, options) {
package/dist/cli.d.mts CHANGED
@@ -2,7 +2,6 @@ import { CliRuntime, CreateCliRuntimeOptions, createCliRuntime } from "./runtime
2
2
  export declare const main: import("citty").CommandDef<import("citty").ArgsDef>;
3
3
  export interface RunCliOptions {
4
4
  rawArgs?: string[];
5
- loadEnv?: boolean;
6
5
  environment?: Record<string, string | undefined>;
7
6
  runtime?: CliRuntime;
8
7
  }
package/dist/cli.mjs CHANGED
@@ -1,11 +1,10 @@
1
- import { CLI_SUBCOMMANDS, resolveUsageTarget } from "./command-registry.mjs";
2
- import { reportCliError } from "./error-handler.mjs";
3
1
  import { createCliRuntime, runWithCliRuntime, useCliRuntime } from "./runtime.mjs";
4
2
  import { parseAuthMode } from "./auth-state.mjs";
5
3
  import { checkCliArgs } from "./cli-args.mjs";
4
+ import { CLI_SUBCOMMANDS, resolveUsageTarget } from "./command-registry.mjs";
6
5
  import { resolveCliEnvironment } from "./environment.mjs";
7
6
  import { applyProfileFromCli } from "./commands/profile-selection.mjs";
8
- import { loadEnvFromCwd } from "./env-file.mjs";
7
+ import { reportCliError } from "./error-handler.mjs";
9
8
  import { resolveOutputOptions, terminalOutputOptions } from "./render/terminal.mjs";
10
9
  import { VERSION, setNoColor, showSplash, withConfiguredOutput } from "./utils.mjs";
11
10
  import process from "node:process";
@@ -97,8 +96,8 @@ async function runCli(opts = {}) {
97
96
  runtime.rawArgs = [...input];
98
97
  return runWithCliRuntime(runtime, async () => {
99
98
  let rawArgs = [...input];
99
+ runtime.jsonOutput = false;
100
100
  try {
101
- if (opts.loadEnv !== false) loadEnvFromCwd();
102
101
  rawArgs = prepareCliArgs(input);
103
102
  runtime.rawArgs = [...rawArgs];
104
103
  const argumentError = await checkCliArgs(main, rawArgs);
@@ -108,6 +107,7 @@ async function runCli(opts = {}) {
108
107
  } catch (error) {
109
108
  await reportCliError(error, {
110
109
  color: stderrColor(runtime, rawArgs),
110
+ json: runtime.jsonOutput,
111
111
  usage: async () => renderUsage(...await resolveUsageTarget(main, rawArgs)),
112
112
  authSources: async () => (await import("./auth.mjs")).formatAuthProvenance()
113
113
  });
@@ -1,6 +1,6 @@
1
1
  import { analyzeCommandMeta } from "../command-meta.mjs";
2
2
  import { terminalOutputOptions } from "../render/terminal.mjs";
3
- import { logger, parseFetchBudget, parseIntegerOption, toCSV } from "../utils.mjs";
3
+ import { logger, parseFetchBudget, parseIntegerOption, setJsonOutput, toCSV } from "../utils.mjs";
4
4
  import { DEFAULT_WINDOW, PERIOD_FLAGS, parseWindowFlags, windowFlagErrorToException } from "../window.mjs";
5
5
  import { analysisNeeds, analyzerTables, resolveAnalysisSource } from "../analysis-local.mjs";
6
6
  import { coverageWarning, renderAnalysis } from "../render/analysis.mjs";
@@ -176,10 +176,10 @@ function makeToolCommand(tool) {
176
176
  ].includes(args.format ?? "table")) throw new Error("Invalid --format. Use table, json, or csv.");
177
177
  const baseParams = buildParams(tool, args);
178
178
  const format = args.json ? "json" : String(args.format ?? "table");
179
+ setJsonOutput(format === "json");
179
180
  const { runAnalysis, siteUrl, anchor } = await resolveAnalysisSource({
180
181
  site: args.site,
181
182
  live: !!args.live,
182
- json: format === "json",
183
183
  label: `analyze ${tool}`,
184
184
  types: [tool],
185
185
  anchorTables: analyzerTables(baseParams),
@@ -1,6 +1,6 @@
1
- import { authCommandMeta } from "../command-meta.mjs";
2
1
  import { useCliRuntime } from "../runtime.mjs";
3
2
  import { clearAuthentication, formatHostedSync, getHostedAccount, parseAuthMode, parseAuthentication, resolveAuthentication, revokeHostedSession, saveAuthentication } from "../auth-state.mjs";
3
+ import { authCommandMeta } from "../command-meta.mjs";
4
4
  import { loadConfig, saveConfig } from "../config.mjs";
5
5
  import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
6
6
  import { ACCESS_NOT_SET_UP, clearTokens, formatAuthProvenance, getAuth, loadServiceAccount, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
@@ -1,7 +1,8 @@
1
- import { bingCommandMeta } from "../command-meta.mjs";
2
1
  import { useCliRuntime } from "../runtime.mjs";
2
+ import { stopError } from "../stop.mjs";
3
3
  import { resolveAuthentication, saveAuthentication } from "../auth-state.mjs";
4
- import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
4
+ import { bingCommandMeta } from "../command-meta.mjs";
5
+ import { OUTPUT_ARGS, applyOutputMode, logger, plural } from "../utils.mjs";
5
6
  import { BING_REDIRECT_URI, clearBingCredentials, getBingClient, loginBingOAuth, saveBingCredentials } from "../bing-auth.mjs";
6
7
  import { BING_DATASETS, dumpBingSite, parseBingDumpOptions, resolveBingSites, unwrapBing } from "../bing-data.mjs";
7
8
  import { dumpHostedBingSite, hostedBingClient, inspectHostedBingUrl, listHostedBingSites, resolveHostedBingSites } from "../bing-hosted.mjs";
@@ -171,7 +172,7 @@ const bingCommand = defineCommand({
171
172
  authenticated: true,
172
173
  verifiedSites: sites.filter((site) => site.isVerified).length
173
174
  }));
174
- else logger.success(`Bing authenticated. ${sites.filter((site) => site.isVerified).length} verified sites.`);
175
+ else logger.success(`Bing authenticated. ${plural(sites.filter((site) => site.isVerified).length, "verified site", "verified sites")}.`);
175
176
  }
176
177
  }),
177
178
  sites: defineCommand({
@@ -250,7 +251,7 @@ const bingCommand = defineCommand({
250
251
  for (const site of sites) {
251
252
  const summary = await dumpHostedBingSite(authentication, site, args.out, options);
252
253
  summaries.push(summary);
253
- if (!json) logger.success(`Bing ${site.siteUrl}: ${summary.files.length} files exported to ${args.out}.`);
254
+ if (!json) logger.success(`Bing ${site.siteUrl}: ${plural(summary.files.length, "file", "files")} exported to ${args.out}.`);
254
255
  }
255
256
  if (json) console.log(JSON.stringify(args["all-sites"] ? {
256
257
  searchEngine: "bing",
@@ -267,7 +268,7 @@ const bingCommand = defineCommand({
267
268
  for (const site of sites) {
268
269
  const summary = await dumpBingSite(client, site, args.out, options);
269
270
  summaries.push(summary);
270
- if (!json) logger.success(`Bing ${site}: ${summary.files.length} files exported to ${args.out}.`);
271
+ if (!json) logger.success(`Bing ${site}: ${plural(summary.files.length, "file", "files")} exported to ${args.out}.`);
271
272
  }
272
273
  if (json) console.log(JSON.stringify(args["all-sites"] ? {
273
274
  searchEngine: "bing",
@@ -323,7 +324,11 @@ const bingCommand = defineCommand({
323
324
  async run({ args }) {
324
325
  applyOutputMode(args);
325
326
  const authentication = await resolveAuthentication();
326
- if (authentication._tag !== "Hosted") throw new Error("Hosted Bing connection verification needs Hosted mode. Pass --mode hosted.");
327
+ if (authentication._tag !== "Hosted") throw stopError({
328
+ code: "HOSTED_MODE_REQUIRED",
329
+ message: "Hosted Bing connection verification needs Hosted mode. Pass --mode hosted.",
330
+ nextCommand: "gscdump auth login --mode hosted"
331
+ });
327
332
  const [site] = await resolveHostedBingSites(authentication, {
328
333
  site: args.site,
329
334
  requireConnected: false
@@ -1,8 +1,7 @@
1
+ import { connectSiteStep, getHostedAccount, resolveAuthentication, siteManagerOf } from "../auth-state.mjs";
1
2
  import { doctorCommandMeta } from "../command-meta.mjs";
2
- import { getHostedAccount, resolveAuthentication } from "../auth-state.mjs";
3
3
  import { loadConfig } from "../config.mjs";
4
- import { resolveCliEnvironment } from "../environment.mjs";
5
- import { parseEnvFile } from "../env-file.mjs";
4
+ import { pickCliEnvironmentValue, resolveCliEnvironment } from "../environment.mjs";
6
5
  import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs";
7
6
  import { getAuth, isStaleServiceAccountPointer, loadTokens, resolveAuth, resolveBYOK, resolveServiceAccount } from "../auth.mjs";
8
7
  import { createLocalStore } from "../local-store.mjs";
@@ -12,73 +11,19 @@ import { currentAccessToken, fetchTokenInfo, redactTokens } from "../token-info.
12
11
  import process from "node:process";
13
12
  import { defineCommand } from "citty";
14
13
  import fs from "node:fs/promises";
15
- import path from "node:path";
16
14
  import { googleSearchConsole } from "gscdump/client";
17
15
  import { resolveSiteInput } from "gscdump";
18
16
  import { ofetch } from "ofetch";
19
17
  const FETCH_TIMEOUT_MS = 5e3;
20
18
  const TIME_SKEW_WARN_MS = 3e5;
21
19
  const WATERMARK_STALE_DAYS_WARN = 7;
22
- const RELEVANT_ENV_KEYS = [
23
- "GSC_ACCESS_TOKEN",
24
- "GSC_CLIENT_ID",
25
- "GSC_CLIENT_SECRET",
26
- "GSC_REFRESH_TOKEN",
27
- "GOOGLE_ACCESS_TOKEN",
28
- "GOOGLE_CLIENT_ID",
29
- "GOOGLE_CLIENT_SECRET",
30
- "GOOGLE_REFRESH_TOKEN",
31
- "GOOGLE_APPLICATION_CREDENTIALS",
32
- "GSC_SERVICE_ACCOUNT_JSON"
33
- ];
34
- function redact(v) {
35
- if (!v) return "<missing>";
36
- if (v.length <= 6) return "***";
37
- return `***${v.slice(-6)}`;
38
- }
39
- async function checkEnv() {
40
- const envPath = path.join(process.cwd(), ".env");
41
- const parsed = parseEnvFile(envPath);
42
- if (!parsed) return {
43
- checks: [{
44
- name: "env",
45
- status: "info",
46
- detail: "no .env (using shell env / saved tokens)"
47
- }],
48
- envKeys: /* @__PURE__ */ new Set()
49
- };
50
- const relevant = RELEVANT_ENV_KEYS.filter((k) => parsed[k] !== void 0);
51
- const envKeys = new Set(relevant);
52
- if (relevant.length === 0) return {
53
- checks: [{
54
- name: "env",
55
- status: "info",
56
- detail: `${displayPath(envPath)} found, no auth vars`
57
- }],
58
- envKeys
59
- };
60
- const inventory = relevant.map((k) => {
61
- const v = parsed[k];
62
- if (k.endsWith("CLIENT_ID")) return `${k}=${v}`;
63
- return `${k}=${redact(v)}`;
64
- });
65
- return {
66
- checks: [{
67
- name: "env",
68
- status: "info",
69
- detail: `${displayPath(envPath)} → ${inventory.join(", ")} (not validated — see auth)`
70
- }],
71
- envKeys
72
- };
73
- }
74
- function describeAuthSource(envKeys, byok) {
20
+ function describeAuthSource(byok) {
75
21
  if (!byok) return "saved tokens";
76
22
  const isAccessToken = typeof byok === "string";
77
- const driver = isAccessToken ? "GSC_ACCESS_TOKEN" : "GSC_REFRESH_TOKEN";
78
- const source = (isAccessToken ? ["GSC_ACCESS_TOKEN", "GOOGLE_ACCESS_TOKEN"] : ["GSC_REFRESH_TOKEN", "GOOGLE_REFRESH_TOKEN"]).some((k) => envKeys.has(k)) ? ".env" : "shell env";
79
- return `environment credentials ${isAccessToken ? "(access-token)" : "(refresh-token)"} from ${source} via ${driver}`;
23
+ const driver = pickCliEnvironmentValue(isAccessToken ? ["GSC_ACCESS_TOKEN", "GOOGLE_ACCESS_TOKEN"] : ["GSC_REFRESH_TOKEN", "GOOGLE_REFRESH_TOKEN"])?.envVar ?? (isAccessToken ? "GSC_ACCESS_TOKEN" : "GSC_REFRESH_TOKEN");
24
+ return `environment credentials ${isAccessToken ? "(access-token)" : "(refresh-token)"} via ${driver}`;
80
25
  }
81
- async function checkAuth(envKeys) {
26
+ async function checkAuth() {
82
27
  const checks = [];
83
28
  const serviceAccount = await resolveServiceAccount().catch((error) => {
84
29
  if (isStaleServiceAccountPointer(error)) {
@@ -153,7 +98,7 @@ async function checkAuth(envKeys) {
153
98
  checks.push({
154
99
  name: "auth",
155
100
  status: "fail",
156
- detail: `${describeAuthSource(envKeys, byok)}: refresh failed: ${current.detail}. Run \`gscdump auth login --force\` and update the source above.`
101
+ detail: `${describeAuthSource(byok)}: refresh failed: ${current.detail}. Run \`gscdump auth login --force\` and update the source above.`
157
102
  });
158
103
  return {
159
104
  checks,
@@ -177,7 +122,7 @@ async function checkAuth(envKeys) {
177
122
  checks.push({
178
123
  name: "auth",
179
124
  status: "pass",
180
- detail: describeAuthSource(envKeys, byok)
125
+ detail: describeAuthSource(byok)
181
126
  });
182
127
  if (info.email) checks.push({
183
128
  name: "auth.account",
@@ -394,7 +339,7 @@ async function checkHostedConnection(authentication) {
394
339
  } : {
395
340
  name: "hosted.sites",
396
341
  status: "warn",
397
- detail: "no hosted Sites. Connect a Site at https://gscdump.com/app/onboarding?step=connect-sites"
342
+ detail: `no hosted Sites. ${connectSiteStep(siteManagerOf(authentication))}`
398
343
  }];
399
344
  return [
400
345
  {
@@ -410,9 +355,9 @@ async function checkHostedConnection(authentication) {
410
355
  ...sitesChecks
411
356
  ];
412
357
  }
413
- async function checkLocalConnection(envKeys) {
358
+ async function checkLocalConnection() {
414
359
  const [authResult, timeChecks, gscApi, indexingApi, siteVerificationApi] = await Promise.all([
415
- checkAuth(envKeys),
360
+ checkAuth(),
416
361
  checkTimeSkew(),
417
362
  checkApiReachable("gsc.api", "https://searchconsole.googleapis.com/$discovery/rest?version=v1"),
418
363
  checkApiReachable("indexing.api", "https://indexing.googleapis.com/$discovery/rest?version=v3"),
@@ -439,14 +384,12 @@ const doctorCommand = defineCommand({
439
384
  const { json } = applyOutputMode(args);
440
385
  const { dataDir } = await createCommandContext();
441
386
  const authentication = await resolveAuthentication();
442
- const envResult = await checkEnv();
443
387
  const [connectionChecks, dataDirChecks, watermarkChecks] = await Promise.all([
444
- authentication._tag === "Hosted" ? checkHostedConnection(authentication) : checkLocalConnection(envResult.envKeys),
388
+ authentication._tag === "Hosted" ? checkHostedConnection(authentication) : checkLocalConnection(),
445
389
  checkDataDir(dataDir),
446
390
  checkStoreWatermarks(dataDir)
447
391
  ]);
448
392
  const all = [
449
- ...envResult.checks,
450
393
  ...connectionChecks,
451
394
  ...dataDirChecks,
452
395
  ...watermarkChecks
@@ -121,8 +121,8 @@ const indexingUrlsCommand = defineCommand({
121
121
  if (limit > MAX_PAGE_SIZE) throw new Error(`--limit must be ${MAX_PAGE_SIZE} or less. Use --all to read every page.`);
122
122
  const search = args.search ? String(args.search) : void 0;
123
123
  applyOutputMode({
124
- json: format !== "table",
125
- quiet: args.quiet
124
+ json: format === "json",
125
+ quiet: Boolean(args.quiet) || format === "csv"
126
126
  });
127
127
  const filters = {
128
128
  ...status ? { status } : {},
@@ -1,3 +1,4 @@
1
+ import { siteManagerOf, siteSettingsPlace } from "../auth-state.mjs";
1
2
  import { terminalOutputOptions } from "../render/terminal.mjs";
2
3
  import { applyOutputMode, logger, readUrlList } from "../utils.mjs";
3
4
  import { renderTable } from "../render/layout.mjs";
@@ -39,12 +40,14 @@ function watchedRow(entry) {
39
40
  dueAt: entry.dueAt.slice(0, 10)
40
41
  };
41
42
  }
42
- const SKIP_HINTS = { inspection_disabled: "gscdump never inspects this Site, so a Watched URL gets no Checkpoint. To use Watched URLs, turn on URL Inspection in the Site settings on gscdump.com." };
43
- function printChange(verb, result, siteUrl) {
43
+ function skipHint(reason, manager) {
44
+ return reason === "inspection_disabled" ? `gscdump never inspects this Site, so a Watched URL gets no Checkpoint. To use Watched URLs, turn on URL Inspection ${siteSettingsPlace(manager)}.` : void 0;
45
+ }
46
+ function printChange(verb, result, siteUrl, manager) {
44
47
  for (const url of result.changed) console.log(`${verb}: ${url}`);
45
48
  for (const url of result.unchanged) console.log(`${verb === "Added" ? "Already watched" : "Not watched"}: ${url}`);
46
49
  for (const { url, reason } of result.skipped) console.log(`Skipped (${reason}): ${url}`);
47
- for (const hint of new Set(result.skipped.map(({ reason }) => SKIP_HINTS[reason]).filter(Boolean))) console.log(hint);
50
+ for (const hint of new Set(result.skipped.map(({ reason }) => skipHint(reason, manager)).filter(Boolean))) console.log(hint);
48
51
  logger.info(`${siteUrl} has ${result.total} of ${result.limit} Watched URLs.`);
49
52
  }
50
53
  async function readUrls(args) {
@@ -133,7 +136,7 @@ const addCommand = defineCommand({
133
136
  async run({ args }) {
134
137
  const { json } = applyOutputMode(args);
135
138
  const urls = await readUrls(args);
136
- const { client, site } = await resolveHostedSite({
139
+ const { client, site, authentication } = await resolveHostedSite({
137
140
  ...args,
138
141
  _: []
139
142
  }, {
@@ -148,7 +151,7 @@ const addCommand = defineCommand({
148
151
  console.log(JSON.stringify(data, null, 2));
149
152
  return;
150
153
  }
151
- printChange("Added", data, site.siteUrl);
154
+ printChange("Added", data, site.siteUrl, siteManagerOf(authentication));
152
155
  }
153
156
  });
154
157
  const removeCommand = defineCommand({
@@ -164,7 +167,7 @@ const removeCommand = defineCommand({
164
167
  async run({ args }) {
165
168
  const { json } = applyOutputMode(args);
166
169
  const urls = await readUrls(args);
167
- const { client, site } = await resolveHostedSite({
170
+ const { client, site, authentication } = await resolveHostedSite({
168
171
  ...args,
169
172
  _: []
170
173
  }, {
@@ -179,7 +182,7 @@ const removeCommand = defineCommand({
179
182
  console.log(JSON.stringify(data, null, 2));
180
183
  return;
181
184
  }
182
- printChange("Removed", data, site.siteUrl);
185
+ printChange("Removed", data, site.siteUrl, siteManagerOf(authentication));
183
186
  }
184
187
  });
185
188
  const indexingWatchCommand = defineCommand({
@@ -1,18 +1,14 @@
1
- import { initCommandMeta } from "../command-meta.mjs";
2
1
  import { useCliRuntime } from "../runtime.mjs";
3
2
  import { saveAuthentication } from "../auth-state.mjs";
3
+ import { initCommandMeta } from "../command-meta.mjs";
4
4
  import { defaultDataDir, loadConfig, saveConfig } from "../config.mjs";
5
- import { applyCliEnvironment } from "../environment.mjs";
6
- import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs";
7
- import { ACCESS_NOT_SET_UP, authenticate, getAuth, loadTokens, resolveBYOK, resolveServiceAccount, saveTokens } from "../auth.mjs";
5
+ import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
6
+ import { ACCESS_NOT_SET_UP, getAuth, loadTokens, resolveBYOK, resolveServiceAccount } from "../auth.mjs";
8
7
  import { loginHosted } from "./auth.mjs";
9
8
  import process from "node:process";
10
9
  import { defineCommand } from "citty";
11
- import fs from "node:fs/promises";
12
- import path from "node:path";
13
- import { confirm, isCancel, text } from "@clack/prompts";
10
+ import { isCancel, text } from "@clack/prompts";
14
11
  import { googleSearchConsole } from "gscdump/client";
15
- const ENV_LINE_RE = /^([^=]+)=(.*)$/;
16
12
  function canPrompt() {
17
13
  return process.stdin.isTTY === true;
18
14
  }
@@ -27,24 +23,6 @@ async function promptDataDir(existing) {
27
23
  if (isCancel(answer)) process.exit(1);
28
24
  return String(answer) || fallback;
29
25
  }
30
- async function loadEnvFile() {
31
- const envPath = path.join(process.cwd(), ".env");
32
- const content = await fs.readFile(envPath, "utf-8").catch(() => null);
33
- if (!content) return null;
34
- const env = {};
35
- for (const line of content.split("\n")) {
36
- const trimmed = line.trim();
37
- if (!trimmed || trimmed.startsWith("#")) continue;
38
- const match = trimmed.match(ENV_LINE_RE);
39
- if (match) {
40
- const key = match[1].trim();
41
- let value = match[2].trim();
42
- if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
43
- env[key] = value;
44
- }
45
- }
46
- return env;
47
- }
48
26
  const initCommand = defineCommand({
49
27
  meta: initCommandMeta,
50
28
  args: {
@@ -121,40 +99,6 @@ const initCommand = defineCommand({
121
99
  printNextSteps();
122
100
  return;
123
101
  }
124
- const envFile = await loadEnvFile();
125
- const envCid = envFile?.GSC_CLIENT_ID ?? envFile?.GOOGLE_CLIENT_ID;
126
- const envSec = envFile?.GSC_CLIENT_SECRET ?? envFile?.GOOGLE_CLIENT_SECRET;
127
- const envRef = envFile?.GSC_REFRESH_TOKEN ?? envFile?.GOOGLE_REFRESH_TOKEN;
128
- const envAcc = envFile?.GSC_ACCESS_TOKEN ?? envFile?.GOOGLE_ACCESS_TOKEN;
129
- if (envCid && envSec && envRef) {
130
- logger.info("Found .env file with credentials");
131
- applyCliEnvironment({
132
- GOOGLE_ACCESS_TOKEN: envAcc,
133
- GOOGLE_CLIENT_ID: envCid,
134
- GOOGLE_CLIENT_SECRET: envSec,
135
- GOOGLE_REFRESH_TOKEN: envRef
136
- });
137
- await saveConfig({
138
- ...config,
139
- clientId: envCid,
140
- clientSecret: envSec,
141
- dataDir: config.dataDir ?? defaultDataDir()
142
- });
143
- const creds = (await authenticate({
144
- clientId: envCid,
145
- clientSecret: envSec
146
- }, false)).credentials;
147
- if (creds.access_token) await saveTokens({
148
- access_token: creds.access_token,
149
- refresh_token: creds.refresh_token || envRef,
150
- expiry_date: creds.expiry_date
151
- });
152
- await saveAuthentication({ _tag: "Local" });
153
- console.log();
154
- logger.success("Setup complete using .env credentials.");
155
- printNextSteps();
156
- return;
157
- }
158
102
  if (!canPrompt()) {
159
103
  if (!await loadTokens()) {
160
104
  logger.error(`Init cannot prompt without a terminal.\n${ACCESS_NOT_SET_UP}`);
@@ -180,7 +124,6 @@ const initCommand = defineCommand({
180
124
  });
181
125
  const sites = await runSmokeTest(await getAuth({ interactive: true }));
182
126
  await saveAuthentication({ _tag: "Local" });
183
- if (config.clientId && config.clientSecret) await maybeWriteEnvFile(config.clientId, config.clientSecret);
184
127
  console.log();
185
128
  logger.success("Setup complete.");
186
129
  printNextSteps(sites);
@@ -229,32 +172,4 @@ async function runSmokeTest(oauth) {
229
172
  logger.success(`Verified: ${sites.length} GSC site(s) accessible`);
230
173
  return sites.flatMap((site) => site.siteUrl ? [site.siteUrl] : []);
231
174
  }
232
- async function maybeWriteEnvFile(clientId, clientSecret) {
233
- const tokens = await loadTokens();
234
- if (!tokens?.refresh_token) return;
235
- const wants = await confirm({
236
- message: "Write a `.env` file with these credentials? (handy for CI / other machines)",
237
- initialValue: false
238
- });
239
- if (isCancel(wants) || !wants) return;
240
- const envPath = path.join(process.cwd(), ".env");
241
- if (await fs.stat(envPath).then(() => true).catch(() => false)) {
242
- const overwrite = await confirm({
243
- message: `${displayPath(envPath)} exists. Overwrite?`,
244
- initialValue: false
245
- });
246
- if (isCancel(overwrite) || !overwrite) {
247
- logger.info(`Skipped — keep credentials at ${displayPath(envPath)} manually if needed`);
248
- return;
249
- }
250
- }
251
- const content = [
252
- `GSC_CLIENT_ID=${clientId}`,
253
- `GSC_CLIENT_SECRET=${clientSecret}`,
254
- `GSC_REFRESH_TOKEN=${tokens.refresh_token}`,
255
- ""
256
- ].join("\n");
257
- await fs.writeFile(envPath, content, { mode: 384 });
258
- logger.success(`Wrote ${displayPath(envPath)}`);
259
- }
260
175
  export { initCommand, printHostedNextSteps, printNextSteps, runSmokeTest };
@@ -1,5 +1,5 @@
1
- import { mcpCommandMeta } from "../command-meta.mjs";
2
1
  import { runWithCliRuntime, useCliRuntime } from "../runtime.mjs";
2
+ import { mcpCommandMeta } from "../command-meta.mjs";
3
3
  import { VERSION } from "../utils.mjs";
4
4
  import { probeAuth } from "../auth.mjs";
5
5
  import { createCommandContext } from "../context.mjs";