@gscdump/cli 3.4.4 → 3.6.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.
Files changed (57) hide show
  1. package/README.md +217 -54
  2. package/bin/gscdump.mjs +6 -1
  3. package/dist/analysis-local.mjs +1 -1
  4. package/dist/auth-state.mjs +112 -0
  5. package/dist/auth.mjs +109 -122
  6. package/dist/bing-auth.mjs +200 -0
  7. package/dist/bing-data.mjs +160 -0
  8. package/dist/bing-hosted.mjs +121 -0
  9. package/dist/cli.d.mts +4 -4
  10. package/dist/cli.mjs +20 -21
  11. package/dist/cloud-google.mjs +92 -0
  12. package/dist/command-meta.mjs +13 -1
  13. package/dist/command-registry.mjs +5 -2
  14. package/dist/commands/analyze.mjs +34 -177
  15. package/dist/commands/auth.mjs +161 -7
  16. package/dist/commands/bing.mjs +337 -0
  17. package/dist/commands/config.mjs +36 -43
  18. package/dist/commands/doctor.mjs +73 -24
  19. package/dist/commands/dump.mjs +1 -1
  20. package/dist/commands/entities.mjs +4 -4
  21. package/dist/commands/indexing.mjs +11 -14
  22. package/dist/commands/init.mjs +1 -1
  23. package/dist/commands/inspect.mjs +3 -3
  24. package/dist/commands/mcp.mjs +16 -2
  25. package/dist/commands/papercut.mjs +76 -0
  26. package/dist/commands/profile-selection.mjs +2 -2
  27. package/dist/commands/profile.mjs +8 -3
  28. package/dist/commands/query.mjs +86 -40
  29. package/dist/commands/report.mjs +5 -3
  30. package/dist/commands/sitemaps.mjs +24 -18
  31. package/dist/commands/skill.mjs +52 -0
  32. package/dist/commands/stats.mjs +58 -32
  33. package/dist/commands/sync.mjs +58 -25
  34. package/dist/config.mjs +39 -4
  35. package/dist/context.mjs +13 -8
  36. package/dist/env-file.mjs +1 -1
  37. package/dist/local-store.mjs +2 -2
  38. package/dist/mcp/errors.mjs +8 -0
  39. package/dist/mcp/handlers/diagnostics.mjs +31 -0
  40. package/dist/mcp/handlers/reports.mjs +38 -11
  41. package/dist/mcp/server/index.mjs +9 -10
  42. package/dist/mcp/types.mjs +8 -3
  43. package/dist/package.mjs +1 -1
  44. package/dist/papercut.mjs +99 -0
  45. package/dist/render/analysis.mjs +98 -0
  46. package/dist/render/charts.mjs +170 -0
  47. package/dist/render/layout.mjs +87 -0
  48. package/dist/render/metrics.mjs +163 -0
  49. package/dist/render/query.mjs +30 -0
  50. package/dist/render/report.mjs +69 -0
  51. package/dist/render/terminal.mjs +25 -0
  52. package/dist/runtime.d.mts +5 -5
  53. package/dist/runtime.mjs +1 -1
  54. package/dist/skill.mjs +45 -0
  55. package/dist/utils.mjs +10 -38
  56. package/package.json +14 -12
  57. package/skills/gscdump/SKILL.md +287 -0
@@ -1,8 +1,11 @@
1
1
  import { configCommandMeta } from "../command-meta.mjs";
2
- import { getConfigPath, loadConfig, saveConfig } from "../config.mjs";
2
+ import { getConfigPath, loadConfig, resolveDataDir, saveConfig } from "../config.mjs";
3
3
  import { OUTPUT_ARGS, applyOutputMode, displayPath, logger, noSubcommandSelected } from "../utils.mjs";
4
+ import { createCommandContext } from "../context.mjs";
4
5
  import process from "node:process";
5
6
  import { defineCommand } from "citty";
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
6
9
  const showCommand = defineCommand({
7
10
  meta: {
8
11
  name: "show",
@@ -11,7 +14,8 @@ const showCommand = defineCommand({
11
14
  args: { ...OUTPUT_ARGS },
12
15
  async run({ args }) {
13
16
  const { json } = applyOutputMode(args);
14
- const config = await loadConfig();
17
+ const config = { ...await loadConfig() };
18
+ if (config.clientSecret) config.clientSecret = config.clientSecret.length <= 6 ? "***" : `***${config.clientSecret.slice(-6)}`;
15
19
  const configPath = getConfigPath();
16
20
  if (json) {
17
21
  console.log(JSON.stringify({
@@ -120,82 +124,71 @@ const validateCommand = defineCommand({
120
124
  args: { ...OUTPUT_ARGS },
121
125
  async run({ args }) {
122
126
  const { json } = applyOutputMode(args);
123
- const { resolveDataDir } = await import("../config.mjs");
124
- const fs = await import("node:fs/promises");
125
127
  const config = await loadConfig();
126
128
  const issues = [];
127
129
  const dataDir = resolveDataDir(config);
128
130
  const dataDirDisplay = displayPath(dataDir);
129
- const stat = await fs.stat(dataDir).catch(() => null);
131
+ const stat = await fs.stat(dataDir).catch((error) => {
132
+ if (error.code === "ENOENT") return void 0;
133
+ issues.push({
134
+ key: "dataDir",
135
+ level: "fail",
136
+ message: `Could not inspect ${dataDirDisplay}: ${error.message}`
137
+ });
138
+ return null;
139
+ });
130
140
  if (stat && !stat.isDirectory()) issues.push({
131
141
  key: "dataDir",
132
142
  level: "fail",
133
143
  message: `${dataDirDisplay} is not a directory`
134
144
  });
135
145
  else if (stat) {
136
- const probe = `${dataDir}/.gscdump-config-probe`;
137
- if (!await fs.writeFile(probe, "").then(() => fs.rm(probe)).then(() => true).catch(() => false)) issues.push({
146
+ if (!await fs.mkdtemp(path.join(dataDir, ".gscdump-config-probe-")).then((probe) => fs.rmdir(probe)).then(() => true).catch(() => false)) issues.push({
138
147
  key: "dataDir",
139
148
  level: "fail",
140
149
  message: `${dataDirDisplay} not writable`
141
150
  });
142
- } else issues.push({
151
+ } else if (stat === void 0) issues.push({
143
152
  key: "dataDir",
144
153
  level: "warn",
145
154
  message: `${dataDirDisplay} does not exist (will be created on first sync)`
146
155
  });
147
156
  if (config.defaultSite) {
148
- if (!!config.clientId && !!config.clientSecret) {
149
- const { createCommandContext } = await import("../context.mjs");
150
- const ctx = await createCommandContext({ needsAuth: true }).catch(() => null);
151
- if (ctx) {
152
- const sites = await ctx.loadSites().catch(() => null);
153
- if (sites && !sites.some((s) => s.siteUrl === config.defaultSite || s.siteUrl.includes(String(config.defaultSite)))) issues.push({
157
+ if (!!config.clientId && !!config.clientSecret || !!config.serviceAccountPath) {
158
+ const sites = await createCommandContext({ needsAuth: true }).then((ctx) => ctx.loadSites()).catch((error) => {
159
+ const message = error instanceof Error ? error.message : String(error);
160
+ issues.push({
154
161
  key: "defaultSite",
155
- level: "fail",
156
- message: `${config.defaultSite} is not in the verified site list`
162
+ level: "warn",
163
+ message: `Could not verify Site: ${message}`
157
164
  });
158
- }
165
+ });
166
+ if (sites && !sites.some((s) => s.siteUrl === config.defaultSite || s.siteUrl.includes(String(config.defaultSite)))) issues.push({
167
+ key: "defaultSite",
168
+ level: "fail",
169
+ message: `${config.defaultSite} is not in the verified site list`
170
+ });
159
171
  } else issues.push({
160
172
  key: "defaultSite",
161
173
  level: "warn",
162
- message: "set, but auth not configured — skipping verification"
174
+ message: "Set, but auth is not configured. Verification skipped."
163
175
  });
164
176
  }
165
- if (config.defaultFormat && !["json", "csv"].includes(config.defaultFormat)) issues.push({
166
- key: "defaultFormat",
167
- level: "fail",
168
- message: `unknown format: ${config.defaultFormat}`
169
- });
170
- const { SearchTypes } = await import("gscdump/query");
171
- const allowedSearchTypes = Object.values(SearchTypes);
172
- if (config.defaultSearchType && !allowedSearchTypes.includes(config.defaultSearchType)) issues.push({
173
- key: "defaultSearchType",
174
- level: "fail",
175
- message: `unknown search type: ${config.defaultSearchType} (allowed: ${allowedSearchTypes.join(", ")})`
176
- });
177
- const allowedDataStates = [
178
- "all",
179
- "final",
180
- "hourly_all"
181
- ];
182
- if (config.defaultDataState && !allowedDataStates.includes(config.defaultDataState)) issues.push({
183
- key: "defaultDataState",
184
- level: "fail",
185
- message: `unknown data state: ${config.defaultDataState} (allowed: ${allowedDataStates.join(", ")})`
186
- });
187
177
  if (config.serviceAccountPath) {
188
- if (!await fs.stat(config.serviceAccountPath).catch(() => null)) issues.push({
178
+ const saPath = config.serviceAccountPath;
179
+ if (!await fs.stat(saPath).then(async (stat) => stat.isFile() && await fs.access(saPath, fs.constants.R_OK).then(() => true)).catch(() => false)) issues.push({
189
180
  key: "serviceAccountPath",
190
181
  level: "fail",
191
- message: `${displayPath(config.serviceAccountPath)} does not exist`
182
+ message: `${displayPath(config.serviceAccountPath)} must point to a readable file`
192
183
  });
193
184
  }
194
185
  if (json) {
186
+ const failed = issues.some((i) => i.level === "fail");
195
187
  console.log(JSON.stringify({
196
- ok: !issues.some((i) => i.level === "fail"),
188
+ ok: !failed,
197
189
  issues
198
190
  }, null, 2));
191
+ if (failed) process.exit(1);
199
192
  return;
200
193
  }
201
194
  if (issues.length === 0) {
@@ -1,3 +1,4 @@
1
+ import { getCloudAccount, resolveAuthentication } from "../auth-state.mjs";
1
2
  import { doctorCommandMeta } from "../command-meta.mjs";
2
3
  import { loadConfig } from "../config.mjs";
3
4
  import { resolveCliEnvironment } from "../environment.mjs";
@@ -9,8 +10,8 @@ import { createCommandContext } from "../context.mjs";
9
10
  import { missingRequiredScopes } from "../auth-scopes.mjs";
10
11
  import process from "node:process";
11
12
  import { defineCommand } from "citty";
12
- import path from "node:path";
13
13
  import fs from "node:fs/promises";
14
+ import path from "node:path";
14
15
  import { googleSearchConsole } from "gscdump/client";
15
16
  import { ofetch } from "ofetch";
16
17
  const FETCH_TIMEOUT_MS = 5e3;
@@ -299,6 +300,9 @@ async function checkGscSites() {
299
300
  status: "fail",
300
301
  detail: `sites() failed: ${sites.message}`
301
302
  }];
303
+ return describeGscSites(sites, config.defaultSite);
304
+ }
305
+ function describeGscSites(sites, defaultSite) {
302
306
  const checks = [];
303
307
  const verified = sites.filter((s) => s.permissionLevel !== "siteUnverifiedUser").length;
304
308
  checks.push({
@@ -306,51 +310,96 @@ async function checkGscSites() {
306
310
  status: "pass",
307
311
  detail: `${sites.length} site(s) accessible (${verified} verified)`
308
312
  });
309
- if (config.defaultSite) {
310
- const match = sites.find((s) => s.siteUrl === config.defaultSite || (s.siteUrl ?? "").includes(String(config.defaultSite)));
313
+ if (defaultSite) {
314
+ const match = sites.find((s) => s.siteUrl === defaultSite || (s.siteUrl ?? "").includes(defaultSite));
311
315
  checks.push(match ? {
312
316
  name: "config.defaultSite",
313
317
  status: "pass",
314
- detail: `${config.defaultSite} ✓`
318
+ detail: `${defaultSite} ✓`
315
319
  } : {
316
320
  name: "config.defaultSite",
317
321
  status: "fail",
318
- detail: `${config.defaultSite} not in verified site list`
322
+ detail: `${defaultSite} not in verified site list`
319
323
  });
320
324
  }
321
325
  return checks;
322
326
  }
327
+ async function checkCloudConnection(authentication) {
328
+ const account = await getCloudAccount(authentication).then((value) => ({
329
+ _tag: "Ok",
330
+ value
331
+ }), (error) => ({
332
+ _tag: "Err",
333
+ detail: error instanceof Error ? error.message : "Hosted authentication failed."
334
+ }));
335
+ if (account._tag === "Err") return [{
336
+ name: "auth",
337
+ status: "fail",
338
+ detail: account.detail
339
+ }, {
340
+ name: "gsc.sites",
341
+ status: "warn",
342
+ detail: "skipped (auth failed)"
343
+ }];
344
+ const sitesChecks = await createCommandContext({ needsAuth: true }).then(async (context) => describeGscSites(await context.loadSites(), context.config.defaultSite)).catch((error) => [{
345
+ name: "gsc.sites",
346
+ status: "fail",
347
+ detail: error instanceof Error ? error.message : "Hosted Sites request failed."
348
+ }]);
349
+ return [
350
+ {
351
+ name: "auth",
352
+ status: "pass",
353
+ detail: `cloud via ${authentication.apiRoot}`
354
+ },
355
+ {
356
+ name: "auth.account",
357
+ status: "pass",
358
+ detail: account.value.user.email
359
+ },
360
+ ...sitesChecks
361
+ ];
362
+ }
363
+ async function checkLocalConnection(envKeys) {
364
+ const [authResult, timeChecks, gscApi, indexingApi, siteVerificationApi] = await Promise.all([
365
+ checkAuth(envKeys),
366
+ checkTimeSkew(),
367
+ checkApiReachable("gsc.api", "https://searchconsole.googleapis.com/$discovery/rest?version=v1"),
368
+ checkApiReachable("indexing.api", "https://indexing.googleapis.com/$discovery/rest?version=v3"),
369
+ checkApiReachable("siteverification.api", "https://www.googleapis.com/discovery/v1/apis/siteVerification/v1/rest")
370
+ ]);
371
+ const sitesChecks = authResult.liveToken ? await checkGscSites() : [{
372
+ name: "gsc.sites",
373
+ status: "warn",
374
+ detail: "skipped (auth failed)"
375
+ }];
376
+ return [
377
+ ...authResult.checks,
378
+ ...timeChecks,
379
+ ...gscApi,
380
+ ...indexingApi,
381
+ ...siteVerificationApi,
382
+ ...sitesChecks
383
+ ];
384
+ }
323
385
  const doctorCommand = defineCommand({
324
386
  meta: doctorCommandMeta,
325
387
  args: { ...OUTPUT_ARGS },
326
388
  async run({ args }) {
327
389
  const { json } = applyOutputMode(args);
328
390
  const { dataDir } = await createCommandContext();
391
+ const authentication = await resolveAuthentication();
329
392
  const envResult = await checkEnv();
330
- const [authResult, timeChecks, dataDirChecks, watermarkChecks, gscApi, indexingApi, siteVerificationApi] = await Promise.all([
331
- checkAuth(envResult.envKeys),
332
- checkTimeSkew(),
393
+ const [connectionChecks, dataDirChecks, watermarkChecks] = await Promise.all([
394
+ authentication._tag === "Cloud" ? checkCloudConnection(authentication) : checkLocalConnection(envResult.envKeys),
333
395
  checkDataDir(dataDir),
334
- checkStoreWatermarks(dataDir),
335
- checkApiReachable("gsc.api", "https://searchconsole.googleapis.com/$discovery/rest?version=v1"),
336
- checkApiReachable("indexing.api", "https://indexing.googleapis.com/$discovery/rest?version=v3"),
337
- checkApiReachable("siteverification.api", "https://www.googleapis.com/discovery/v1/apis/siteVerification/v1/rest")
396
+ checkStoreWatermarks(dataDir)
338
397
  ]);
339
- const sitesChecks = authResult.liveToken ? await checkGscSites() : [{
340
- name: "gsc.sites",
341
- status: "warn",
342
- detail: "skipped (auth failed)"
343
- }];
344
398
  const all = [
345
399
  ...envResult.checks,
346
- ...authResult.checks,
347
- ...timeChecks,
400
+ ...connectionChecks,
348
401
  ...dataDirChecks,
349
- ...watermarkChecks,
350
- ...gscApi,
351
- ...indexingApi,
352
- ...siteVerificationApi,
353
- ...sitesChecks
402
+ ...watermarkChecks
354
403
  ];
355
404
  if (json) {
356
405
  console.log(JSON.stringify({
@@ -5,8 +5,8 @@ import { createCommandContext } from "../context.mjs";
5
5
  import { readParquetRows } from "../native-duckdb.mjs";
6
6
  import process from "node:process";
7
7
  import { defineCommand } from "citty";
8
- import path from "node:path";
9
8
  import fs from "node:fs/promises";
9
+ import path from "node:path";
10
10
  const DEFAULT_OUT = "./gscdump-export";
11
11
  const FORMATS = [
12
12
  "parquet",
@@ -1,5 +1,5 @@
1
1
  import { entitiesCommandMeta } from "../command-meta.mjs";
2
- import { OUTPUT_ARGS, applyOutputMode, logger, progressBar, runWithConcurrency } from "../utils.mjs";
2
+ import { OUTPUT_ARGS, applyOutputMode, logger, parseIntegerOption, progressBar, runWithConcurrency } from "../utils.mjs";
3
3
  import { createCommandContext } from "../context.mjs";
4
4
  import process from "node:process";
5
5
  import { defineCommand } from "citty";
@@ -58,6 +58,8 @@ const inspectSubCommand = defineCommand({
58
58
  },
59
59
  async run({ args }) {
60
60
  const { json, quiet } = applyOutputMode(args);
61
+ const limit = parseIntegerOption(args.limit, "--limit") ?? INSPECTION_QPD_PER_PROPERTY;
62
+ const concurrency = parseIntegerOption(args.concurrency, "--concurrency") ?? 4;
61
63
  const ctx = await createCommandContext({
62
64
  needsAuth: true,
63
65
  needsStore: true
@@ -65,8 +67,6 @@ const inspectSubCommand = defineCommand({
65
67
  const client = ctx.client;
66
68
  const store = ctx.store;
67
69
  const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
68
- const limit = args.limit ? Number.parseInt(String(args.limit), 10) : INSPECTION_QPD_PER_PROPERTY;
69
- const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 4);
70
70
  const urls = (await readUrlList({ file: args.file ? String(args.file) : void 0 })).slice(0, limit);
71
71
  if (urls.length === 0) {
72
72
  logger.warn("No URLs to inspect.");
@@ -207,6 +207,7 @@ const indexingSnapshotSubCommand = defineCommand({
207
207
  },
208
208
  async run({ args }) {
209
209
  const { quiet } = applyOutputMode(args);
210
+ const concurrency = parseIntegerOption(args.concurrency, "--concurrency") ?? 4;
210
211
  const ctx = await createCommandContext({
211
212
  needsAuth: true,
212
213
  needsStore: true
@@ -214,7 +215,6 @@ const indexingSnapshotSubCommand = defineCommand({
214
215
  const client = ctx.client;
215
216
  const store = ctx.store;
216
217
  const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
217
- const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 4);
218
218
  const urls = await readUrlList({ file: args.file ? String(args.file) : void 0 });
219
219
  if (urls.length === 0) {
220
220
  logger.warn("No URLs to fetch metadata for.");
@@ -1,5 +1,5 @@
1
1
  import { indexingCommandMeta } from "../command-meta.mjs";
2
- import { OUTPUT_ARGS, applyOutputMode, logger, readUrlList } from "../utils.mjs";
2
+ import { OUTPUT_ARGS, applyOutputMode, logger, parseIntegerOption, readUrlList } from "../utils.mjs";
3
3
  import { createCommandContext } from "../context.mjs";
4
4
  import { gscErrorHandler } from "../error-handler.mjs";
5
5
  import { loadSitemapUrls } from "../sitemap.mjs";
@@ -10,11 +10,6 @@ const RETRIES_ARG = { retries: {
10
10
  type: "string",
11
11
  description: "Override per-call retry count (default: 3)"
12
12
  } };
13
- function parseRetries(v) {
14
- if (v == null || v === "") return void 0;
15
- const n = Number.parseInt(String(v), 10);
16
- return Number.isFinite(n) && n >= 0 ? n : void 0;
17
- }
18
13
  async function resolveUrlSource(args) {
19
14
  const fromSitemap = args["from-sitemap"];
20
15
  if (fromSitemap) {
@@ -46,7 +41,7 @@ const submitCommand = defineCommand({
46
41
  applyOutputMode(args);
47
42
  const ctx = await createCommandContext({
48
43
  needsAuth: true,
49
- fetchOptions: { retry: parseRetries(args.retries) }
44
+ fetchOptions: { retry: parseIntegerOption(args.retries, "--retries", 0) }
50
45
  });
51
46
  const result = await requestIndexing(ctx.client, args.url, { type: "URL_UPDATED" }).catch(gscErrorHandler);
52
47
  if (args.json) {
@@ -75,7 +70,7 @@ const removeCommand = defineCommand({
75
70
  applyOutputMode(args);
76
71
  const ctx = await createCommandContext({
77
72
  needsAuth: true,
78
- fetchOptions: { retry: parseRetries(args.retries) }
73
+ fetchOptions: { retry: parseIntegerOption(args.retries, "--retries", 0) }
79
74
  });
80
75
  const result = await requestIndexing(ctx.client, args.url, { type: "URL_DELETED" }).catch(gscErrorHandler);
81
76
  if (args.json) {
@@ -210,6 +205,9 @@ const batchCommand = defineCommand({
210
205
  },
211
206
  async run({ args }) {
212
207
  applyOutputMode(args);
208
+ const retry = parseIntegerOption(args.retries, "--retries", 0);
209
+ const delayMs = parseIntegerOption(args["delay-ms"], "--delay-ms", 0) ?? 100;
210
+ const concurrency = parseIntegerOption(args.concurrency, "--concurrency") ?? 1;
213
211
  const urls = await resolveUrlSource(args);
214
212
  if (urls.length === 0) {
215
213
  logger.error("No URLs provided. Pass URLs as args, --file, --from-sitemap, or stdin.");
@@ -228,10 +226,8 @@ const batchCommand = defineCommand({
228
226
  if (urls.length > INDEXING_DAILY_QUOTA && !args.json && !args.quiet) logger.warn(`Proceeding with ${urls.length} URLs (over the ${INDEXING_DAILY_QUOTA}/day quota). Excess will fail.`);
229
227
  const ctx = await createCommandContext({
230
228
  needsAuth: true,
231
- fetchOptions: { retry: parseRetries(args.retries) }
229
+ fetchOptions: { retry }
232
230
  });
233
- const delayMs = Number.parseInt(String(args["delay-ms"]), 10);
234
- const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 1);
235
231
  if (!args.json && !args.quiet) logger.info(`Submitting ${urls.length} URLs (${type}) ...`);
236
232
  const results = await batchRequestIndexing(ctx.client, urls, {
237
233
  type,
@@ -285,6 +281,9 @@ const batchStatusCommand = defineCommand({
285
281
  },
286
282
  async run({ args }) {
287
283
  applyOutputMode(args);
284
+ const retry = parseIntegerOption(args.retries, "--retries", 0);
285
+ const delayMs = parseIntegerOption(args["delay-ms"], "--delay-ms", 0) ?? 100;
286
+ const concurrency = parseIntegerOption(args.concurrency, "--concurrency") ?? 1;
288
287
  const urls = await resolveUrlSource(args);
289
288
  if (urls.length === 0) {
290
289
  logger.error("No URLs provided. Pass URLs as args, --file, --from-sitemap, or stdin.");
@@ -292,10 +291,8 @@ const batchStatusCommand = defineCommand({
292
291
  }
293
292
  const ctx = await createCommandContext({
294
293
  needsAuth: true,
295
- fetchOptions: { retry: parseRetries(args.retries) }
294
+ fetchOptions: { retry }
296
295
  });
297
- const delayMs = Number.parseInt(String(args["delay-ms"]), 10);
298
- const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 1);
299
296
  if (!args.json && !args.quiet) logger.info(`Fetching status for ${urls.length} URLs ...`);
300
297
  const results = await runSequentialBatch(urls, (url) => getIndexingMetadata(ctx.client, url), {
301
298
  delayMs,
@@ -5,8 +5,8 @@ import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs"
5
5
  import { authenticate, getAuthCredentials, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
6
6
  import process from "node:process";
7
7
  import { defineCommand } from "citty";
8
- import path from "node:path";
9
8
  import fs from "node:fs/promises";
9
+ import path from "node:path";
10
10
  import { confirm, isCancel, text } from "@clack/prompts";
11
11
  import { googleSearchConsole } from "gscdump/client";
12
12
  const ENV_LINE_RE = /^([^=]+)=(.*)$/;
@@ -1,5 +1,5 @@
1
1
  import { inspectCommandMeta } from "../command-meta.mjs";
2
- import { OUTPUT_ARGS, applyOutputMode, logger, readUrlList } from "../utils.mjs";
2
+ import { OUTPUT_ARGS, applyOutputMode, logger, parseIntegerOption, readUrlList } from "../utils.mjs";
3
3
  import { createCommandContext } from "../context.mjs";
4
4
  import { gscErrorHandler } from "../error-handler.mjs";
5
5
  import { loadSitemapUrls } from "../sitemap.mjs";
@@ -116,6 +116,8 @@ const batchCommand = defineCommand({
116
116
  },
117
117
  async run({ args }) {
118
118
  const { json, quiet } = applyOutputMode(args);
119
+ const delayMs = parseIntegerOption(args["delay-ms"], "--delay-ms", 0) ?? 200;
120
+ const concurrency = parseIntegerOption(args.concurrency, "--concurrency") ?? 1;
119
121
  let urls;
120
122
  if (args["from-sitemap"]) {
121
123
  const result = await loadSitemapUrls(String(args["from-sitemap"]));
@@ -132,8 +134,6 @@ const batchCommand = defineCommand({
132
134
  }
133
135
  const ctx = await createCommandContext({ needsAuth: true });
134
136
  const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
135
- const delayMs = Number.parseInt(String(args["delay-ms"]), 10);
136
- const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 1);
137
137
  if (!quiet) logger.info(`Inspecting ${urls.length} URLs ...`);
138
138
  const results = await batchInspectUrls(ctx.client, siteUrl, urls, {
139
139
  delayMs,
@@ -1,12 +1,19 @@
1
+ import { getCloudAccount, resolveAuthentication } from "../auth-state.mjs";
1
2
  import { mcpCommandMeta } from "../command-meta.mjs";
2
3
  import { loadConfig } from "../config.mjs";
3
4
  import { VERSION } from "../utils.mjs";
4
- import { loadTokens, resolveAuth, resolveBYOK, resolveServiceAccount } from "../auth.mjs";
5
+ import { loadTokens, resolveBYOK, resolveServiceAccount } from "../auth.mjs";
6
+ import { createCommandContext } from "../context.mjs";
5
7
  import { createGscMcpServer } from "../mcp/server/index.mjs";
6
8
  import process from "node:process";
7
9
  import { defineCommand } from "citty";
8
10
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
11
  async function checkAuth() {
12
+ const authentication = await resolveAuthentication();
13
+ if (authentication._tag === "Cloud") {
14
+ await getCloudAccount(authentication);
15
+ return { ok: true };
16
+ }
10
17
  if (await resolveServiceAccount().then(Boolean).catch(() => false)) return { ok: true };
11
18
  if (resolveBYOK()) return { ok: true };
12
19
  const config = await loadConfig();
@@ -46,7 +53,14 @@ const mcpCommand = defineCommand({
46
53
  const server = createGscMcpServer({
47
54
  name: "gscdump",
48
55
  version: VERSION,
49
- getAuth: () => resolveAuth({ interactive: false })
56
+ getContext: async () => {
57
+ const ctx = await createCommandContext({ needsAuth: true });
58
+ return {
59
+ authentication: ctx.authentication,
60
+ auth: ctx.auth,
61
+ client: ctx.client
62
+ };
63
+ }
50
64
  });
51
65
  const transport = new StdioServerTransport();
52
66
  await server.connect(transport);
@@ -0,0 +1,76 @@
1
+ import { useCliRuntime } from "../runtime.mjs";
2
+ import { papercutCommandMeta } from "../command-meta.mjs";
3
+ import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
4
+ import { buildPapercutBody, resolvePapercutUrl, submitPapercut } from "../papercut.mjs";
5
+ import process from "node:process";
6
+ import { defineCommand } from "citty";
7
+ import { confirm, isCancel } from "@clack/prompts";
8
+ const papercutCommand = defineCommand({
9
+ meta: papercutCommandMeta,
10
+ args: {
11
+ command: {
12
+ type: "string",
13
+ description: "The gscdump command that misbehaved, for example \"report triage\""
14
+ },
15
+ comment: {
16
+ type: "string",
17
+ description: "What you ran, what you expected, what happened, any workaround (sanitized)"
18
+ },
19
+ agent: {
20
+ type: "string",
21
+ description: "Reporter name, for example \"Claude Code\" or a person's handle"
22
+ },
23
+ intent: {
24
+ type: "string",
25
+ description: "bug (default) or improvement",
26
+ default: "bug"
27
+ },
28
+ yes: {
29
+ type: "boolean",
30
+ alias: "y",
31
+ default: false,
32
+ description: "Submit without confirmation"
33
+ },
34
+ ...OUTPUT_ARGS
35
+ },
36
+ async run({ args }) {
37
+ const { json } = applyOutputMode(args);
38
+ const built = buildPapercutBody({
39
+ command: args.command,
40
+ comment: args.comment,
41
+ agent: args.agent,
42
+ intent: args.intent
43
+ });
44
+ if (built._tag === "Err") {
45
+ logger.error(`Invalid papercut: ${built.message}`);
46
+ logger.info("Pass --command, --comment, and --agent. Read `gscdump papercut --help` for limits.");
47
+ process.exit(2);
48
+ }
49
+ if (!args.yes) {
50
+ if (!process.stdin.isTTY || json) {
51
+ logger.error("Pass --yes to submit without a prompt.");
52
+ process.exit(2);
53
+ }
54
+ const ok = await confirm({ message: `Send this papercut about "${built.body.command}" to gscdump.com?` });
55
+ if (isCancel(ok) || !ok) {
56
+ logger.info("Papercut cancelled.");
57
+ process.exit(0);
58
+ }
59
+ }
60
+ const url = resolvePapercutUrl(useCliRuntime().environment);
61
+ const result = await submitPapercut(built.body, {
62
+ fetch: globalThis.fetch,
63
+ url
64
+ });
65
+ if (result._tag === "Err") {
66
+ logger.error(result.message);
67
+ process.exit(1);
68
+ }
69
+ if (json) {
70
+ console.log(JSON.stringify(result.receipt));
71
+ return;
72
+ }
73
+ logger.success(`Papercut saved: ${result.receipt.id} (${result.receipt.status})`);
74
+ }
75
+ });
76
+ export { papercutCommand };
@@ -1,9 +1,9 @@
1
1
  import { useCliRuntime } from "../runtime.mjs";
2
2
  import { setConfigDir } from "../config.mjs";
3
3
  import { resolveCliEnvironment } from "../environment.mjs";
4
- import fs from "node:fs";
5
- import os from "node:os";
6
4
  import path from "node:path";
5
+ import os from "node:os";
6
+ import fs from "node:fs";
7
7
  const ROOT_DIR = path.join(os.homedir(), ".config", "gscdump");
8
8
  const PROFILES_DIR = path.join(ROOT_DIR, "profiles");
9
9
  const ACTIVE_MARKER = path.join(ROOT_DIR, "active-profile");
@@ -1,13 +1,13 @@
1
- import { profileCommandMeta } from "../command-meta.mjs";
2
1
  import { useCliRuntime } from "../runtime.mjs";
2
+ import { profileCommandMeta } from "../command-meta.mjs";
3
3
  import { getConfigDir, setConfigDir } from "../config.mjs";
4
4
  import { resolveCliEnvironment } from "../environment.mjs";
5
5
  import { ACTIVE_MARKER, PROFILES_DIR, ROOT_DIR, getProfileDir, readActiveMarkerSync } from "./profile-selection.mjs";
6
6
  import { OUTPUT_ARGS, applyOutputMode, displayPath, logger, noSubcommandSelected } from "../utils.mjs";
7
7
  import process from "node:process";
8
8
  import { defineCommand } from "citty";
9
- import path from "node:path";
10
9
  import fs from "node:fs/promises";
10
+ import path from "node:path";
11
11
  import { confirm, isCancel } from "@clack/prompts";
12
12
  function resolveActiveProfile() {
13
13
  return useCliRuntime().activeProfileOverride ?? resolveCliEnvironment().profile ?? readActiveMarkerSync();
@@ -48,7 +48,12 @@ async function adoptCurrentConfigAsProfile(name) {
48
48
  recursive: true,
49
49
  mode: 448
50
50
  });
51
- for (const f of ["tokens.json", "config.json"]) {
51
+ for (const f of [
52
+ "tokens.json",
53
+ "config.json",
54
+ "authentication.json",
55
+ "bing-tokens.json"
56
+ ]) {
52
57
  const src = path.join(currentDir, f);
53
58
  const dst = path.join(targetDir, f);
54
59
  if (await fs.stat(src).then(() => true).catch(() => false)) await fs.rename(src, dst);