@gscdump/cli 3.5.0 → 3.6.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.
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,13 +1,17 @@
1
+ import { useCliRuntime } from "../runtime.mjs";
2
+ import { clearAuthentication, getCloudAccount, parseAuthMode, parseAuthentication, resolveAuthentication, saveAuthentication } from "../auth-state.mjs";
1
3
  import { authCommandMeta } from "../command-meta.mjs";
2
4
  import { loadConfig, saveConfig } from "../config.mjs";
3
5
  import { OUTPUT_ARGS, applyOutputMode, logger, noSubcommandSelected } from "../utils.mjs";
4
6
  import { authenticate, clearTokens, formatAuthProvenance, getAuth, getAuthCredentials, loadServiceAccount, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
5
7
  import { missingRequiredScopes } from "../auth-scopes.mjs";
8
+ import { clearBingCredentials, getBingClient, inspectBingCredentials } from "../bing-auth.mjs";
6
9
  import { runSmokeTest } from "./init.mjs";
7
10
  import { adoptCurrentConfigAsProfile, profileNameFromEmail, resolveActiveProfile } from "./profile.mjs";
8
11
  import process from "node:process";
9
12
  import { defineCommand } from "citty";
10
13
  import path from "node:path";
14
+ import { isCancel, password } from "@clack/prompts";
11
15
  import { ofetch } from "ofetch";
12
16
  const AUTH_SUBCOMMANDS = [
13
17
  "status",
@@ -16,6 +20,38 @@ const AUTH_SUBCOMMANDS = [
16
20
  "refresh",
17
21
  "scopes"
18
22
  ];
23
+ const MODE_ARG = {
24
+ type: "string",
25
+ description: "Authentication mode: cloud or local"
26
+ };
27
+ function applyAuthMode(args) {
28
+ const mode = parseAuthMode(args.mode);
29
+ if (mode) useCliRuntime().authModeOverride = mode;
30
+ }
31
+ async function requireLocalAuth(args) {
32
+ applyAuthMode(args);
33
+ if ((await resolveAuthentication())._tag === "Cloud") throw new Error("Cloud authentication uses an API key. OAuth scopes and token refresh require --mode local.");
34
+ }
35
+ async function loginCloud(args) {
36
+ const env = useCliRuntime().environment;
37
+ let apiKey = String(args["api-key"] ?? env.GSCDUMP_API_KEY ?? "");
38
+ if (!apiKey) {
39
+ if (!process.stdin.isTTY) throw new Error("Cloud login requires --api-key or GSCDUMP_API_KEY.");
40
+ const answer = await password({ message: "gscdump user API key" });
41
+ if (isCancel(answer) || typeof answer !== "string") throw new Error("Login cancelled.");
42
+ apiKey = answer;
43
+ }
44
+ const state = parseAuthentication({
45
+ _tag: "Cloud",
46
+ apiKey,
47
+ apiRoot: String(args["api-root"] ?? env.GSCDUMP_API_ROOT ?? "https://gscdump.com/api")
48
+ });
49
+ if (state._tag !== "Cloud") throw new Error("Cloud login requires a gscdump user API key.");
50
+ const account = await getCloudAccount(state);
51
+ await saveAuthentication(state);
52
+ logger.success(`Cloud authentication saved for ${account.user.email}`);
53
+ logger.info("Google and Bing commands use connections saved on gscdump.com.");
54
+ }
19
55
  async function fetchTokenInfo(accessToken) {
20
56
  return ofetch("https://oauth2.googleapis.com/tokeninfo", { query: { access_token: accessToken } }).catch(() => null);
21
57
  }
@@ -40,11 +76,90 @@ async function resolveLiveAuthState() {
40
76
  }
41
77
  async function runStatus(args) {
42
78
  const { json } = applyOutputMode(args);
79
+ applyAuthMode(args);
80
+ const authentication = await resolveAuthentication();
81
+ if (authentication._tag === "Cloud") {
82
+ const capabilities = {
83
+ google: [
84
+ "sites",
85
+ "query",
86
+ "sync",
87
+ "inspect",
88
+ "sitemaps",
89
+ "analyze",
90
+ "report"
91
+ ],
92
+ bing: [
93
+ "sites",
94
+ "dump",
95
+ "inspect",
96
+ "login",
97
+ "status",
98
+ "verify"
99
+ ],
100
+ cloud: [
101
+ "sitemaps current",
102
+ "sitemaps history",
103
+ "sitemaps membership",
104
+ "sitemaps lastmod",
105
+ "sitemaps export"
106
+ ],
107
+ local: ["indexing", "sites verification"]
108
+ };
109
+ const account = await getCloudAccount(authentication).then((value) => ({
110
+ _tag: "Ok",
111
+ value
112
+ }), (error) => ({
113
+ _tag: "Err",
114
+ detail: error instanceof Error ? error.message : "Hosted authentication failed."
115
+ }));
116
+ if (account._tag === "Err") {
117
+ if (json) console.log(JSON.stringify({
118
+ authenticated: false,
119
+ mode: "cloud",
120
+ apiRoot: authentication.apiRoot,
121
+ error: account.detail
122
+ }, null, 2));
123
+ else {
124
+ logger.warn(`Cloud status check failed: ${account.detail}`);
125
+ logger.info(`API: ${authentication.apiRoot}`);
126
+ logger.info("Fix connectivity or the API key, then run `gscdump auth status` again.");
127
+ }
128
+ return;
129
+ }
130
+ if (json) console.log(JSON.stringify({
131
+ authenticated: true,
132
+ mode: "cloud",
133
+ account: account.value.user.email,
134
+ apiRoot: authentication.apiRoot,
135
+ sites: account.value.sites,
136
+ capabilities
137
+ }, null, 2));
138
+ else {
139
+ logger.success(`Authenticated with cloud: ${account.value.user.email}`);
140
+ console.log(` API: ${authentication.apiRoot}`);
141
+ console.log(` Sites: ${account.value.sites.length}`);
142
+ console.log(" Google and Bing use connections saved on gscdump.com.");
143
+ console.log(` Cloud commands: ${capabilities.cloud.join(", ")}`);
144
+ console.log(" Google indexing and Site Verification require --mode local.");
145
+ }
146
+ return;
147
+ }
43
148
  const { byok, tokens, tokenInfo, scopes, missing } = await resolveLiveAuthState();
149
+ const bingCredentials = await inspectBingCredentials();
150
+ const bingResult = bingCredentials._tag === "Missing" ? null : await getBingClient().then((client) => client.getUserSites({ signal: AbortSignal.timeout(1e4) })).catch(() => null);
151
+ const bing = {
152
+ configured: bingCredentials._tag !== "Missing",
153
+ authenticated: bingResult?.ok === true,
154
+ source: bingCredentials._tag === "Missing" ? null : bingCredentials._tag
155
+ };
44
156
  const byokKind = byok ? typeof byok === "string" ? "access-token" : "refresh-token" : null;
45
157
  if (json) {
46
158
  console.log(JSON.stringify({
47
- authenticated: !!tokens || !!byok,
159
+ authenticated: !!tokens || !!byok || bing.authenticated,
160
+ mode: "local",
161
+ googleAuthenticated: !!tokens || !!byok,
162
+ bing,
48
163
  source: byok ? "byok" : tokens ? "saved-tokens" : null,
49
164
  byokKind,
50
165
  scopes,
@@ -58,6 +173,8 @@ async function runStatus(args) {
58
173
  }, null, 2));
59
174
  return;
60
175
  }
176
+ if (bing.authenticated) logger.success(`Bing authenticated locally (${bing.source})`);
177
+ else if (bing.configured) logger.warn("Bing credentials failed verification. Run `gscdump bing login` again.");
61
178
  const reportScopes = () => {
62
179
  if (scopes.length === 0) return;
63
180
  console.log(` Scopes:`);
@@ -75,6 +192,10 @@ async function runStatus(args) {
75
192
  return;
76
193
  }
77
194
  if (!tokens) {
195
+ if (bing.authenticated) {
196
+ logger.info("Google credentials are missing. Run `gscdump auth login --mode local` to connect Google.");
197
+ return;
198
+ }
78
199
  logger.warn("Not authenticated");
79
200
  logger.info("Run `gscdump init` (full setup) or `gscdump auth login` (OAuth only)");
80
201
  logger.info("Or set GSC_ACCESS_TOKEN / GSC_CLIENT_ID + GSC_CLIENT_SECRET + GSC_REFRESH_TOKEN env vars");
@@ -100,7 +221,10 @@ async function runStatus(args) {
100
221
  }
101
222
  const statusCommand = defineCommand({
102
223
  meta: authCommandMeta.status,
103
- args: { ...OUTPUT_ARGS },
224
+ args: {
225
+ ...OUTPUT_ARGS,
226
+ mode: MODE_ARG
227
+ },
104
228
  async run({ args }) {
105
229
  await runStatus(args);
106
230
  }
@@ -110,9 +234,13 @@ const refreshCommand = defineCommand({
110
234
  name: "refresh",
111
235
  description: "Force-refresh saved OAuth tokens (no-op for BYOK)"
112
236
  },
113
- args: { ...OUTPUT_ARGS },
237
+ args: {
238
+ ...OUTPUT_ARGS,
239
+ mode: MODE_ARG
240
+ },
114
241
  async run({ args }) {
115
242
  applyOutputMode(args);
243
+ await requireLocalAuth(args);
116
244
  if (resolveBYOK()) {
117
245
  logger.info("BYOK detected; refresh handled per-call by the SDK");
118
246
  return;
@@ -141,6 +269,15 @@ const loginCommand = defineCommand({
141
269
  meta: authCommandMeta.login,
142
270
  args: {
143
271
  ...OUTPUT_ARGS,
272
+ "mode": MODE_ARG,
273
+ "api-key": {
274
+ type: "string",
275
+ description: "gscdump user API key; defaults to GSCDUMP_API_KEY"
276
+ },
277
+ "api-root": {
278
+ type: "string",
279
+ description: "Cloud API root; defaults to GSCDUMP_API_ROOT or https://gscdump.com/api"
280
+ },
144
281
  "force": {
145
282
  type: "boolean",
146
283
  alias: "f",
@@ -150,7 +287,7 @@ const loginCommand = defineCommand({
150
287
  "browser": {
151
288
  type: "boolean",
152
289
  default: true,
153
- description: "Use loopback browser flow. Pass --no-browser for device-code (headless)."
290
+ description: "Open the authorization URL automatically. Pass --no-browser to open it yourself."
154
291
  },
155
292
  "service-account": {
156
293
  type: "string",
@@ -159,7 +296,14 @@ const loginCommand = defineCommand({
159
296
  },
160
297
  async run({ args }) {
161
298
  applyOutputMode(args);
299
+ const runtime = useCliRuntime();
300
+ const requestedMode = parseAuthMode(args.mode) ?? runtime.authModeOverride ?? parseAuthMode(runtime.environment.GSCDUMP_AUTH_MODE);
301
+ if (requestedMode === "cloud" || !requestedMode && (args["api-key"] || (await resolveAuthentication())._tag === "Cloud")) {
302
+ await loginCloud(args);
303
+ return;
304
+ }
162
305
  if (resolveBYOK() && !args.force) {
306
+ await saveAuthentication({ _tag: "Local" });
163
307
  logger.info("BYOK env vars detected, no login needed (--force to override)");
164
308
  return;
165
309
  }
@@ -176,11 +320,11 @@ const loginCommand = defineCommand({
176
320
  const config = await loadConfig();
177
321
  config.serviceAccountPath = saPath;
178
322
  await saveConfig(config);
323
+ await saveAuthentication({ _tag: "Local" });
179
324
  logger.success(`Service-account verified: ${jwt.email ?? "OK"}`);
180
325
  logger.info(`Saved path to config: ${saPath}`);
181
326
  return;
182
327
  }
183
- if (args.force) await clearTokens();
184
328
  const oauth = await getAuth({
185
329
  interactive: true,
186
330
  noBrowser: args.browser === false,
@@ -191,6 +335,7 @@ const loginCommand = defineCommand({
191
335
  });
192
336
  logger.success("Logged in");
193
337
  await runSmokeTest(oauth);
338
+ await saveAuthentication({ _tag: "Local" });
194
339
  if (!resolveActiveProfile()) {
195
340
  const tokens = await loadTokens();
196
341
  const info = tokens?.access_token ? await fetchTokenInfo(tokens.access_token) : null;
@@ -215,6 +360,8 @@ const logoutCommand = defineCommand({
215
360
  async run({ args }) {
216
361
  applyOutputMode(args);
217
362
  await clearTokens();
363
+ await clearBingCredentials();
364
+ await clearAuthentication();
218
365
  const config = await loadConfig();
219
366
  if (config.serviceAccountPath) {
220
367
  delete config.serviceAccountPath;
@@ -228,9 +375,13 @@ const scopesCommand = defineCommand({
228
375
  name: "scopes",
229
376
  description: "Print granted OAuth scopes (one per line); exits 1 if any required scope is missing"
230
377
  },
231
- args: { ...OUTPUT_ARGS },
378
+ args: {
379
+ ...OUTPUT_ARGS,
380
+ mode: MODE_ARG
381
+ },
232
382
  async run({ args }) {
233
383
  const { json } = applyOutputMode(args);
384
+ await requireLocalAuth(args);
234
385
  const { liveToken, scopes, missing } = await resolveLiveAuthState();
235
386
  if (!liveToken) {
236
387
  if (json) console.log(JSON.stringify({
@@ -250,7 +401,10 @@ const scopesCommand = defineCommand({
250
401
  });
251
402
  const authCommand = defineCommand({
252
403
  meta: authCommandMeta.auth,
253
- args: { ...OUTPUT_ARGS },
404
+ args: {
405
+ ...OUTPUT_ARGS,
406
+ mode: MODE_ARG
407
+ },
254
408
  subCommands: {
255
409
  status: statusCommand,
256
410
  login: loginCommand,
@@ -0,0 +1,337 @@
1
+ import { useCliRuntime } from "../runtime.mjs";
2
+ import { resolveAuthentication, saveAuthentication } from "../auth-state.mjs";
3
+ import { bingCommandMeta } from "../command-meta.mjs";
4
+ import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
5
+ import { BING_REDIRECT_URI, clearBingCredentials, getBingClient, loginBingOAuth, saveBingCredentials } from "../bing-auth.mjs";
6
+ import { BING_DATASETS, dumpBingSite, parseBingDumpOptions, resolveBingSites, unwrapBing } from "../bing-data.mjs";
7
+ import { dumpHostedBingSite, hostedBingClient, inspectHostedBingUrl, listHostedBingSites, resolveHostedBingSites } from "../bing-hosted.mjs";
8
+ import process from "node:process";
9
+ import { defineCommand } from "citty";
10
+ import { isCancel, password } from "@clack/prompts";
11
+ import open from "open";
12
+ import { bingWebmaster } from "gscdump/bing";
13
+ const siteArg = {
14
+ type: "string",
15
+ alias: "s",
16
+ description: "Site ID or URL from `gscdump bing sites`"
17
+ };
18
+ const bingCommand = defineCommand({
19
+ meta: bingCommandMeta,
20
+ subCommands: {
21
+ login: defineCommand({
22
+ meta: {
23
+ name: "login",
24
+ description: "Save a Bing API key or authorize with browser OAuth"
25
+ },
26
+ args: {
27
+ "site": siteArg,
28
+ "api-key": {
29
+ type: "string",
30
+ description: "Bing Webmaster API key. Defaults to BING_API_KEY or a password prompt"
31
+ },
32
+ "oauth": {
33
+ type: "boolean",
34
+ description: "Use a registered Bing OAuth client"
35
+ },
36
+ "client-id": {
37
+ type: "string",
38
+ description: "Bing OAuth client ID. Defaults to BING_CLIENT_ID"
39
+ },
40
+ "client-secret": {
41
+ type: "string",
42
+ description: "Bing OAuth client secret. Defaults to BING_CLIENT_SECRET"
43
+ },
44
+ "redirect-uri": {
45
+ type: "string",
46
+ description: `Registered loopback callback. Defaults to BING_REDIRECT_URI or ${BING_REDIRECT_URI}`
47
+ },
48
+ "browser": {
49
+ type: "boolean",
50
+ default: true,
51
+ description: "Open the browser. Use --no-browser to print the login URL"
52
+ },
53
+ ...OUTPUT_ARGS
54
+ },
55
+ async run({ args }) {
56
+ const { json } = applyOutputMode(args);
57
+ const env = useCliRuntime().environment;
58
+ const authentication = await resolveAuthentication();
59
+ if (authentication._tag === "Cloud") {
60
+ if (args.oauth || args["api-key"] || args["client-id"] || args["client-secret"] || args["redirect-uri"]) throw new Error("Local Bing credentials require --mode local. Hosted login uses `gscdump auth login --mode cloud`.");
61
+ if (!args.site) throw new Error("Choose --site SITE_ID from `gscdump bing sites` to connect Bing.");
62
+ const [site] = await resolveHostedBingSites(authentication, {
63
+ site: args.site,
64
+ requireConnected: false
65
+ });
66
+ if (site.connection._tag === "connected") {
67
+ console.log(JSON.stringify({
68
+ searchEngine: "bing",
69
+ connection: site.connection
70
+ }, null, 2));
71
+ return;
72
+ }
73
+ const url = new URL("/auth/bing", authentication.apiRoot);
74
+ url.searchParams.set("siteId", site.siteId);
75
+ process.stderr.write(`Open this URL to connect Bing:\n${url}\n`);
76
+ if (args.browser) await open(url.toString()).catch(() => {
77
+ logger.warn("The browser could not open. Use the printed login URL.");
78
+ });
79
+ if (json) console.log(JSON.stringify({
80
+ searchEngine: "bing",
81
+ connection: site.connection,
82
+ authorizationUrl: url.toString()
83
+ }));
84
+ else logger.info("Complete Bing authorization in your browser. Then run `gscdump bing status --site SITE_ID`.");
85
+ return;
86
+ }
87
+ if (args.oauth) {
88
+ if (args["api-key"]) throw new Error("Choose --oauth or --api-key.");
89
+ await loginBingOAuth({
90
+ clientId: args["client-id"] ?? env.BING_CLIENT_ID ?? "",
91
+ clientSecret: args["client-secret"] ?? env.BING_CLIENT_SECRET ?? "",
92
+ redirectUri: args["redirect-uri"] ?? env.BING_REDIRECT_URI ?? "http://127.0.0.1:53683/oauth/bing",
93
+ async openUrl(url) {
94
+ process.stderr.write(`Open this URL to authorize Bing:\n${url}\n`);
95
+ if (args.browser) await open(url).catch(() => {
96
+ logger.warn("The browser could not open. Use the printed login URL.");
97
+ });
98
+ }
99
+ });
100
+ } else {
101
+ if (args["client-id"] || args["client-secret"] || args["redirect-uri"]) throw new Error("If you use OAuth client options, add --oauth.");
102
+ let apiKey = (args["api-key"] ?? env.BING_API_KEY)?.trim();
103
+ if (!apiKey && process.stdin.isTTY && !json) {
104
+ const value = await password({ message: "Bing Webmaster API key" });
105
+ if (isCancel(value) || typeof value !== "string") throw new Error("Bing login cancelled.");
106
+ apiKey = value.trim();
107
+ }
108
+ if (!apiKey) throw new Error("Set BING_API_KEY or use --oauth with BING_CLIENT_ID and BING_CLIENT_SECRET.");
109
+ unwrapBing(await bingWebmaster({ apiKey }).getUserSites({ signal: AbortSignal.timeout(3e4) }));
110
+ await saveBingCredentials({
111
+ _tag: "ApiKey",
112
+ apiKey
113
+ });
114
+ }
115
+ await saveAuthentication({ _tag: "Local" });
116
+ if (json) console.log(JSON.stringify({
117
+ searchEngine: "bing",
118
+ authenticated: true
119
+ }));
120
+ else logger.success("Bing credentials saved. Run `gscdump bing sites`.");
121
+ }
122
+ }),
123
+ logout: defineCommand({
124
+ meta: {
125
+ name: "logout",
126
+ description: "Remove saved Bing credentials from the current profile"
127
+ },
128
+ args: OUTPUT_ARGS,
129
+ async run({ args }) {
130
+ const { json } = applyOutputMode(args);
131
+ if ((await resolveAuthentication())._tag === "Cloud") throw new Error("Hosted authentication is shared with Google. Use `gscdump auth logout` to remove it.");
132
+ await clearBingCredentials();
133
+ const env = useCliRuntime().environment;
134
+ const environmentCredentials = Boolean(env.BING_API_KEY?.trim() || env.BING_ACCESS_TOKEN?.trim());
135
+ if (json) console.log(JSON.stringify({
136
+ searchEngine: "bing",
137
+ savedCredentialsRemoved: true,
138
+ environmentCredentials
139
+ }));
140
+ else logger.success(`Saved Bing credentials removed.${environmentCredentials ? " Unset BING_API_KEY and BING_ACCESS_TOKEN to stop environment authentication." : ""}`);
141
+ }
142
+ }),
143
+ status: defineCommand({
144
+ meta: {
145
+ name: "status",
146
+ description: "Check Bing authentication against the live API"
147
+ },
148
+ args: {
149
+ site: siteArg,
150
+ ...OUTPUT_ARGS
151
+ },
152
+ async run({ args }) {
153
+ const { json } = applyOutputMode(args);
154
+ const authentication = await resolveAuthentication();
155
+ if (authentication._tag === "Cloud") {
156
+ const sites = args.site ? await resolveHostedBingSites(authentication, {
157
+ site: args.site,
158
+ requireConnected: false
159
+ }) : await listHostedBingSites(authentication);
160
+ console.log(JSON.stringify({
161
+ searchEngine: "bing",
162
+ mode: "cloud",
163
+ sites
164
+ }, null, 2));
165
+ return;
166
+ }
167
+ const client = await getBingClient();
168
+ const sites = unwrapBing(await client.getUserSites({ signal: AbortSignal.timeout(3e4) }));
169
+ if (json) console.log(JSON.stringify({
170
+ searchEngine: "bing",
171
+ authenticated: true,
172
+ verifiedSites: sites.filter((site) => site.isVerified).length
173
+ }));
174
+ else logger.success(`Bing authenticated. ${sites.filter((site) => site.isVerified).length} verified sites.`);
175
+ }
176
+ }),
177
+ sites: defineCommand({
178
+ meta: {
179
+ name: "sites",
180
+ description: "List Bing sites and verification details"
181
+ },
182
+ args: OUTPUT_ARGS,
183
+ async run({ args }) {
184
+ applyOutputMode(args);
185
+ const authentication = await resolveAuthentication();
186
+ if (authentication._tag === "Cloud") {
187
+ console.log(JSON.stringify({
188
+ searchEngine: "bing",
189
+ sites: await listHostedBingSites(authentication)
190
+ }, null, 2));
191
+ return;
192
+ }
193
+ const client = await getBingClient();
194
+ const sites = unwrapBing(await client.getUserSites({ signal: AbortSignal.timeout(3e4) }));
195
+ console.log(JSON.stringify({
196
+ searchEngine: "bing",
197
+ sites
198
+ }, null, 2));
199
+ }
200
+ }),
201
+ dump: defineCommand({
202
+ meta: {
203
+ name: "dump",
204
+ description: "Download Bing traffic, pages, keywords, and crawl data"
205
+ },
206
+ args: {
207
+ "site": siteArg,
208
+ "all-sites": {
209
+ type: "boolean",
210
+ description: "Dump every verified Bing site"
211
+ },
212
+ "out": {
213
+ type: "string",
214
+ alias: "o",
215
+ default: "./bing-export",
216
+ description: "Output directory, with one directory per site"
217
+ },
218
+ "format": {
219
+ type: "string",
220
+ alias: "F",
221
+ default: "json",
222
+ description: "File format: json, ndjson, csv"
223
+ },
224
+ "datasets": {
225
+ type: "string",
226
+ description: `Comma-separated datasets: ${BING_DATASETS.join(", ")}. Default: all except crawl-issues`
227
+ },
228
+ "start": {
229
+ type: "string",
230
+ description: "Keep returned dates on or after YYYY-MM-DD"
231
+ },
232
+ "end": {
233
+ type: "string",
234
+ description: "Keep returned dates on or before YYYY-MM-DD"
235
+ },
236
+ ...OUTPUT_ARGS
237
+ },
238
+ async run({ args }) {
239
+ const { json } = applyOutputMode(args);
240
+ const options = parseBingDumpOptions(args);
241
+ const authentication = await resolveAuthentication();
242
+ if (authentication._tag === "Cloud") {
243
+ if (options.datasets.includes("crawl-issues")) throw new Error("Bing crawl issues require local authentication. Use --mode local.");
244
+ if (options.start && options.end && Date.parse(options.end) - Date.parse(options.start) > 316224e5) throw new Error("Hosted Bing exports support at most 366 days. Use a shorter --start and --end range.");
245
+ const sites = await resolveHostedBingSites(authentication, {
246
+ site: args.site,
247
+ allSites: args["all-sites"]
248
+ });
249
+ const summaries = [];
250
+ for (const site of sites) {
251
+ const summary = await dumpHostedBingSite(authentication, site, args.out, options);
252
+ summaries.push(summary);
253
+ if (!json) logger.success(`Bing ${site.siteUrl}: ${summary.files.length} files exported to ${args.out}.`);
254
+ }
255
+ if (json) console.log(JSON.stringify(args["all-sites"] ? {
256
+ searchEngine: "bing",
257
+ sites: summaries
258
+ } : summaries[0], null, 2));
259
+ return;
260
+ }
261
+ const client = await getBingClient();
262
+ const sites = await resolveBingSites(client, {
263
+ site: args.site,
264
+ allSites: args["all-sites"]
265
+ });
266
+ const summaries = [];
267
+ for (const site of sites) {
268
+ const summary = await dumpBingSite(client, site, args.out, options);
269
+ summaries.push(summary);
270
+ if (!json) logger.success(`Bing ${site}: ${summary.files.length} files exported to ${args.out}.`);
271
+ }
272
+ if (json) console.log(JSON.stringify(args["all-sites"] ? {
273
+ searchEngine: "bing",
274
+ sites: summaries
275
+ } : summaries[0], null, 2));
276
+ }
277
+ }),
278
+ inspect: defineCommand({
279
+ meta: {
280
+ name: "inspect",
281
+ description: "Read Bing Indexing Evidence for one URL"
282
+ },
283
+ args: {
284
+ url: {
285
+ type: "positional",
286
+ required: true,
287
+ description: "Page URL to inspect"
288
+ },
289
+ site: {
290
+ ...siteArg,
291
+ required: true
292
+ },
293
+ ...OUTPUT_ARGS
294
+ },
295
+ async run({ args }) {
296
+ applyOutputMode(args);
297
+ const url = URL.parse(args.url);
298
+ if (!url || !["http:", "https:"].includes(url.protocol)) throw new Error("Use a full HTTP or HTTPS page URL.");
299
+ const authentication = await resolveAuthentication();
300
+ if (authentication._tag === "Cloud") {
301
+ const [site] = await resolveHostedBingSites(authentication, { site: args.site });
302
+ console.log(JSON.stringify(await inspectHostedBingUrl(authentication, site.siteId, args.url), null, 2));
303
+ return;
304
+ }
305
+ const client = await getBingClient();
306
+ const [site] = await resolveBingSites(client, { site: args.site });
307
+ const evidence = unwrapBing(await client.getIndexingEvidence(site, args.url, { signal: AbortSignal.timeout(3e4) }));
308
+ console.log(JSON.stringify(evidence, null, 2));
309
+ }
310
+ }),
311
+ verify: defineCommand({
312
+ meta: {
313
+ name: "verify",
314
+ description: "Check Bing site verification and activate a hosted connection"
315
+ },
316
+ args: {
317
+ site: {
318
+ ...siteArg,
319
+ required: true
320
+ },
321
+ ...OUTPUT_ARGS
322
+ },
323
+ async run({ args }) {
324
+ applyOutputMode(args);
325
+ const authentication = await resolveAuthentication();
326
+ if (authentication._tag !== "Cloud") throw new Error("Hosted Bing connection verification requires --mode cloud.");
327
+ const [site] = await resolveHostedBingSites(authentication, {
328
+ site: args.site,
329
+ requireConnected: false
330
+ });
331
+ const result = await hostedBingClient(authentication).verifySiteBingConnection({ params: { siteId: site.siteId } });
332
+ console.log(JSON.stringify(result.data, null, 2));
333
+ }
334
+ })
335
+ }
336
+ });
337
+ export { bingCommand };