@gscdump/cli 4.2.3 → 4.3.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.
- package/README.md +1 -1
- package/dist/auth-scopes.mjs +4 -4
- package/dist/auth-state.mjs +84 -41
- package/dist/auth.mjs +35 -59
- package/dist/bing-hosted.mjs +4 -4
- package/dist/command-meta.mjs +2 -2
- package/dist/commands/auth.mjs +71 -86
- package/dist/commands/bing.mjs +10 -10
- package/dist/commands/doctor.mjs +64 -14
- package/dist/commands/init.mjs +31 -15
- package/dist/commands/mcp.mjs +13 -7
- package/dist/commands/query.mjs +44 -1
- package/dist/commands/sites.mjs +32 -19
- package/dist/context.mjs +4 -6
- package/dist/dump-bing.mjs +1 -1
- package/dist/error-handler.mjs +1 -1
- package/dist/hosted-auth.mjs +10 -51
- package/dist/hosted-query.mjs +31 -0
- package/dist/hosted-site.mjs +6 -6
- package/dist/mcp/errors.mjs +4 -14
- package/dist/mcp/handlers/diagnostics.mjs +0 -31
- package/dist/package.mjs +1 -1
- package/dist/route.mjs +10 -4
- package/dist/runtime.d.mts +1 -1
- package/dist/sync-plan.mjs +1 -1
- package/package.json +7 -7
- package/skills/gscdump/SKILL.md +77 -62
- package/dist/cloud-google.mjs +0 -99
package/dist/commands/auth.mjs
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { authCommandMeta } from "../command-meta.mjs";
|
|
2
2
|
import { useCliRuntime } from "../runtime.mjs";
|
|
3
|
-
import { clearAuthentication, formatHostedSync,
|
|
3
|
+
import { clearAuthentication, formatHostedSync, getHostedAccount, parseAuthMode, parseAuthentication, resolveAuthentication, revokeHostedSession, saveAuthentication } from "../auth-state.mjs";
|
|
4
4
|
import { loadConfig, saveConfig } from "../config.mjs";
|
|
5
5
|
import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import { GOOGLE_NOT_CONNECTED, clearTokens, formatAuthProvenance, getAuth, loadServiceAccount, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
|
|
6
|
+
import { ACCESS_NOT_SET_UP, clearTokens, formatAuthProvenance, getAuth, loadServiceAccount, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
|
|
8
7
|
import { missingRequiredScopes } from "../auth-scopes.mjs";
|
|
9
8
|
import { clearBingCredentials, getBingClient, inspectBingCredentials } from "../bing-auth.mjs";
|
|
9
|
+
import { loginWithHostedSession } from "../hosted-auth.mjs";
|
|
10
10
|
import { currentAccessToken, fetchTokenInfo, redactTokens } from "../token-info.mjs";
|
|
11
11
|
import { runSmokeTest } from "./init.mjs";
|
|
12
12
|
import { adoptCurrentConfigAsProfile, profileNameFromEmail, resolveActiveProfile } from "./profile.mjs";
|
|
13
13
|
import process from "node:process";
|
|
14
14
|
import { defineCommand } from "citty";
|
|
15
15
|
import path from "node:path";
|
|
16
|
-
import { setTimeout } from "node:timers/promises";
|
|
17
16
|
import open from "open";
|
|
17
|
+
import { setTimeout } from "node:timers/promises";
|
|
18
18
|
const MODE_ARG = {
|
|
19
19
|
type: "string",
|
|
20
|
-
description: "
|
|
20
|
+
description: "Access mode: local or hosted"
|
|
21
21
|
};
|
|
22
22
|
function applyAuthMode(args) {
|
|
23
23
|
const mode = parseAuthMode(args.mode);
|
|
@@ -25,45 +25,62 @@ function applyAuthMode(args) {
|
|
|
25
25
|
}
|
|
26
26
|
async function requireLocalAuth(args) {
|
|
27
27
|
applyAuthMode(args);
|
|
28
|
-
if ((await resolveAuthentication())._tag === "
|
|
28
|
+
if ((await resolveAuthentication())._tag === "Hosted") throw new Error("Google scopes and token refresh apply to Local mode only. Hosted mode uses a gscdump.com CLI session.");
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
const HOSTED_MODE_NOTE = "Hosted mode reads your gscdump.com record. Commands that call Google need Local mode.";
|
|
31
|
+
const HOSTED_COMMANDS = [
|
|
32
|
+
"sites",
|
|
33
|
+
"query",
|
|
34
|
+
"sitemaps current",
|
|
35
|
+
"sitemaps history",
|
|
36
|
+
"sitemaps membership",
|
|
37
|
+
"sitemaps lastmod",
|
|
38
|
+
"sitemaps export",
|
|
39
|
+
"indexing urls",
|
|
40
|
+
"bing login",
|
|
41
|
+
"bing sites",
|
|
42
|
+
"bing status",
|
|
43
|
+
"bing dump",
|
|
44
|
+
"bing inspect",
|
|
45
|
+
"bing verify"
|
|
46
|
+
];
|
|
47
|
+
async function loginHosted(args) {
|
|
31
48
|
const env = useCliRuntime().environment;
|
|
32
49
|
const apiKey = String(args["api-key"] ?? env.GSCDUMP_API_KEY ?? "");
|
|
33
50
|
const apiRoot = String(args["api-root"] ?? env.GSCDUMP_API_ROOT ?? "https://gscdump.com/api");
|
|
34
51
|
if (!apiKey) {
|
|
35
52
|
if (apiRoot.replace(/\/+$/, "") !== "https://gscdump.com/api") throw new Error("Browser login uses gscdump.com. Supply --api-key for a custom API root.");
|
|
36
|
-
const sessionId = await
|
|
53
|
+
const sessionId = await loginWithHostedSession({
|
|
37
54
|
request: fetch,
|
|
38
55
|
now: Date.now,
|
|
39
56
|
wait: setTimeout,
|
|
40
57
|
authorize: async (url) => {
|
|
41
|
-
logger.info(`Open this URL to
|
|
58
|
+
logger.info(`Open this URL to link the CLI to gscdump.com:\n${url}`);
|
|
42
59
|
if (args.browser !== false) await open(url).catch((error) => logger.warn(`Browser could not open: ${error.message}. Open the URL above.`));
|
|
43
60
|
}
|
|
44
61
|
});
|
|
45
62
|
const state = parseAuthentication({
|
|
46
|
-
_tag: "
|
|
63
|
+
_tag: "Hosted",
|
|
47
64
|
apiRoot,
|
|
48
65
|
sessionId
|
|
49
66
|
});
|
|
50
|
-
if (state._tag !== "
|
|
51
|
-
const account = await
|
|
67
|
+
if (state._tag !== "Hosted") throw new Error("Hosted login did not return a CLI session.");
|
|
68
|
+
const account = await getHostedAccount(state);
|
|
52
69
|
await saveAuthentication(state);
|
|
53
|
-
logger.success(`
|
|
54
|
-
logger.info(
|
|
70
|
+
logger.success(`Hosted mode saved for ${account.user.email}`);
|
|
71
|
+
logger.info(HOSTED_MODE_NOTE);
|
|
55
72
|
return;
|
|
56
73
|
}
|
|
57
74
|
const state = parseAuthentication({
|
|
58
|
-
_tag: "
|
|
75
|
+
_tag: "Hosted",
|
|
59
76
|
apiKey,
|
|
60
77
|
apiRoot
|
|
61
78
|
});
|
|
62
|
-
if (state._tag !== "
|
|
63
|
-
const account = await
|
|
79
|
+
if (state._tag !== "Hosted") throw new Error("Hosted login needs a gscdump user API key.");
|
|
80
|
+
const account = await getHostedAccount(state);
|
|
64
81
|
await saveAuthentication(state);
|
|
65
|
-
logger.success(`
|
|
66
|
-
logger.info(
|
|
82
|
+
logger.success(`Hosted mode saved for ${account.user.email}`);
|
|
83
|
+
logger.info(HOSTED_MODE_NOTE);
|
|
67
84
|
}
|
|
68
85
|
async function resolveLiveAuthState() {
|
|
69
86
|
const tokens = await loadTokens();
|
|
@@ -78,7 +95,7 @@ async function resolveLiveAuthState() {
|
|
|
78
95
|
const tokenInfo = verified?.kind === "valid" ? verified.info : null;
|
|
79
96
|
const failure = current?.kind === "failed" ? current.detail : verified?.kind === "invalid" ? verified.detail : null;
|
|
80
97
|
const scopes = tokenInfo?.scope ? tokenInfo.scope.split(/\s+/).filter(Boolean) : [];
|
|
81
|
-
const missing = missingRequiredScopes(scopes
|
|
98
|
+
const missing = missingRequiredScopes(scopes);
|
|
82
99
|
return {
|
|
83
100
|
byok,
|
|
84
101
|
tokens: tokens && !byok ? await loadTokens() : tokens,
|
|
@@ -93,86 +110,54 @@ async function runStatus(args) {
|
|
|
93
110
|
const { json } = applyOutputMode(args);
|
|
94
111
|
applyAuthMode(args);
|
|
95
112
|
const authentication = await resolveAuthentication();
|
|
96
|
-
if (authentication._tag === "
|
|
97
|
-
const
|
|
98
|
-
google: [
|
|
99
|
-
"sites",
|
|
100
|
-
"query",
|
|
101
|
-
"sync",
|
|
102
|
-
"inspect",
|
|
103
|
-
"sitemaps",
|
|
104
|
-
"analyze",
|
|
105
|
-
"report"
|
|
106
|
-
],
|
|
107
|
-
bing: [
|
|
108
|
-
"sites",
|
|
109
|
-
"dump",
|
|
110
|
-
"inspect",
|
|
111
|
-
"login",
|
|
112
|
-
"status",
|
|
113
|
-
"verify"
|
|
114
|
-
],
|
|
115
|
-
cloud: [
|
|
116
|
-
"sitemaps current",
|
|
117
|
-
"sitemaps history",
|
|
118
|
-
"sitemaps membership",
|
|
119
|
-
"sitemaps lastmod",
|
|
120
|
-
"sitemaps export"
|
|
121
|
-
],
|
|
122
|
-
local: ["indexing", "sites verification"]
|
|
123
|
-
};
|
|
124
|
-
const account = await getCloudAccount(authentication).then((value) => ({
|
|
113
|
+
if (authentication._tag === "Hosted") {
|
|
114
|
+
const account = await getHostedAccount(authentication).then((value) => ({
|
|
125
115
|
_tag: "Ok",
|
|
126
116
|
value
|
|
127
117
|
}), (error) => ({
|
|
128
118
|
_tag: "Err",
|
|
129
|
-
detail: error instanceof Error ? error.message : "Hosted
|
|
119
|
+
detail: error instanceof Error ? error.message : "Hosted credentials failed."
|
|
130
120
|
}));
|
|
131
121
|
if (account._tag === "Err") {
|
|
132
122
|
if (json) console.log(JSON.stringify({
|
|
133
123
|
authenticated: false,
|
|
134
|
-
mode: "
|
|
124
|
+
mode: "hosted",
|
|
135
125
|
apiRoot: authentication.apiRoot,
|
|
136
126
|
error: account.detail
|
|
137
127
|
}, null, 2));
|
|
138
128
|
else {
|
|
139
|
-
logger.warn(`
|
|
129
|
+
logger.warn(`Hosted status check failed: ${account.detail}`);
|
|
140
130
|
logger.info(`API: ${authentication.apiRoot}`);
|
|
141
131
|
logger.info("Fix connectivity or the API key, then run `gscdump auth status` again.");
|
|
142
132
|
}
|
|
143
133
|
return;
|
|
144
134
|
}
|
|
145
|
-
const
|
|
146
|
-
_tag: "Ok",
|
|
147
|
-
sites: sites.filter((site) => site.registered)
|
|
148
|
-
}), (error) => ({
|
|
149
|
-
_tag: "Err",
|
|
150
|
-
detail: error instanceof Error ? error.message : "Hosted Site list failed."
|
|
151
|
-
}));
|
|
135
|
+
const sites = account.value.sites;
|
|
152
136
|
if (json) console.log(JSON.stringify({
|
|
153
137
|
authenticated: true,
|
|
154
|
-
mode: "
|
|
138
|
+
mode: "hosted",
|
|
155
139
|
account: account.value.user.email,
|
|
156
140
|
apiRoot: authentication.apiRoot,
|
|
157
|
-
sites:
|
|
158
|
-
|
|
141
|
+
sites: sites.map((site) => ({
|
|
142
|
+
siteId: site.siteId,
|
|
143
|
+
siteUrl: site.siteUrl
|
|
144
|
+
})),
|
|
145
|
+
hostedSync: sites.map((site) => ({
|
|
159
146
|
siteUrl: site.siteUrl,
|
|
160
147
|
syncStatus: site.syncStatus ?? null,
|
|
161
148
|
syncProgress: site.syncProgress ?? null,
|
|
162
149
|
oldestDateSynced: site.oldestDateSynced ?? null,
|
|
163
150
|
newestDateSynced: site.newestDateSynced ?? null
|
|
164
|
-
}))
|
|
165
|
-
|
|
151
|
+
})),
|
|
152
|
+
commands: HOSTED_COMMANDS
|
|
166
153
|
}, null, 2));
|
|
167
154
|
else {
|
|
168
|
-
logger.success(`
|
|
155
|
+
logger.success(`Hosted mode: ${account.value.user.email}`);
|
|
169
156
|
console.log(` API: ${authentication.apiRoot}`);
|
|
170
|
-
console.log(` Sites: ${
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
console.log(
|
|
174
|
-
console.log(` Cloud commands: ${capabilities.cloud.join(", ")}`);
|
|
175
|
-
console.log(" Google indexing and Site Verification require --mode local.");
|
|
157
|
+
console.log(` Sites: ${sites.length}`);
|
|
158
|
+
for (const site of sites) console.log(` ${site.siteUrl} ${formatHostedSync(site)}`);
|
|
159
|
+
console.log(` ${HOSTED_MODE_NOTE}`);
|
|
160
|
+
console.log(` Hosted commands: ${HOSTED_COMMANDS.join(", ")}`);
|
|
176
161
|
}
|
|
177
162
|
return;
|
|
178
163
|
}
|
|
@@ -193,8 +178,8 @@ async function runStatus(args) {
|
|
|
193
178
|
googleAuthenticated,
|
|
194
179
|
googleError: failure,
|
|
195
180
|
bing,
|
|
196
|
-
source: byok ? "
|
|
197
|
-
byokKind,
|
|
181
|
+
source: byok ? "env" : tokens ? "saved-tokens" : null,
|
|
182
|
+
envCredential: byokKind,
|
|
198
183
|
scopes,
|
|
199
184
|
tokenAccount: tokenInfo?.email ?? null,
|
|
200
185
|
tokens: tokens ? {
|
|
@@ -219,8 +204,8 @@ async function runStatus(args) {
|
|
|
219
204
|
}
|
|
220
205
|
};
|
|
221
206
|
if (byok) {
|
|
222
|
-
if (googleAuthenticated) logger.success(`Authenticated
|
|
223
|
-
else logger.warn(`
|
|
207
|
+
if (googleAuthenticated) logger.success(`Authenticated with environment credentials (${byokKind})`);
|
|
208
|
+
else logger.warn(`Environment credentials (${byokKind}) failed verification: ${failure}`);
|
|
224
209
|
if (tokenInfo?.email) console.log(` Account: ${tokenInfo.email}`);
|
|
225
210
|
reportScopes();
|
|
226
211
|
return;
|
|
@@ -230,7 +215,7 @@ async function runStatus(args) {
|
|
|
230
215
|
logger.info("Google credentials are missing. Run `gscdump auth login --mode local` to connect Google.");
|
|
231
216
|
return;
|
|
232
217
|
}
|
|
233
|
-
logger.warn(
|
|
218
|
+
logger.warn(ACCESS_NOT_SET_UP);
|
|
234
219
|
return;
|
|
235
220
|
}
|
|
236
221
|
const hasAccess = !!tokens.access_token;
|
|
@@ -262,7 +247,7 @@ const statusCommand = defineCommand({
|
|
|
262
247
|
const refreshCommand = defineCommand({
|
|
263
248
|
meta: {
|
|
264
249
|
name: "refresh",
|
|
265
|
-
description: "Force-refresh saved OAuth tokens (no-op for
|
|
250
|
+
description: "Force-refresh saved OAuth tokens (no-op for environment credentials)"
|
|
266
251
|
},
|
|
267
252
|
args: {
|
|
268
253
|
...OUTPUT_ARGS,
|
|
@@ -272,7 +257,7 @@ const refreshCommand = defineCommand({
|
|
|
272
257
|
applyOutputMode(args);
|
|
273
258
|
await requireLocalAuth(args);
|
|
274
259
|
if (resolveBYOK()) {
|
|
275
|
-
logger.info("
|
|
260
|
+
logger.info("Environment credentials found. The SDK refreshes them for each call.");
|
|
276
261
|
return;
|
|
277
262
|
}
|
|
278
263
|
const tokens = await loadTokens();
|
|
@@ -298,11 +283,11 @@ const loginCommand = defineCommand({
|
|
|
298
283
|
"mode": MODE_ARG,
|
|
299
284
|
"api-key": {
|
|
300
285
|
type: "string",
|
|
301
|
-
description: "gscdump user API key; defaults to GSCDUMP_API_KEY"
|
|
286
|
+
description: "gscdump user API key for Hosted mode; defaults to GSCDUMP_API_KEY"
|
|
302
287
|
},
|
|
303
288
|
"api-root": {
|
|
304
289
|
type: "string",
|
|
305
|
-
description: "
|
|
290
|
+
description: "Hosted API root; defaults to GSCDUMP_API_ROOT or https://gscdump.com/api"
|
|
306
291
|
},
|
|
307
292
|
"force": {
|
|
308
293
|
type: "boolean",
|
|
@@ -324,13 +309,13 @@ const loginCommand = defineCommand({
|
|
|
324
309
|
applyOutputMode(args);
|
|
325
310
|
const runtime = useCliRuntime();
|
|
326
311
|
const requestedMode = parseAuthMode(args.mode) ?? runtime.authModeOverride ?? parseAuthMode(runtime.environment.GSCDUMP_AUTH_MODE);
|
|
327
|
-
if (requestedMode === "
|
|
328
|
-
await
|
|
312
|
+
if (requestedMode === "hosted" || !requestedMode && (args["api-key"] || (await resolveAuthentication())._tag === "Hosted")) {
|
|
313
|
+
await loginHosted(args);
|
|
329
314
|
return;
|
|
330
315
|
}
|
|
331
316
|
if (resolveBYOK() && !args.force) {
|
|
332
317
|
await saveAuthentication({ _tag: "Local" });
|
|
333
|
-
logger.info("
|
|
318
|
+
logger.info("Google credentials found in the environment. No login is needed. Pass --force to log in again.");
|
|
334
319
|
return;
|
|
335
320
|
}
|
|
336
321
|
if (args["service-account"]) {
|
|
@@ -385,9 +370,9 @@ const logoutCommand = defineCommand({
|
|
|
385
370
|
args: { ...OUTPUT_ARGS },
|
|
386
371
|
async run({ args }) {
|
|
387
372
|
applyOutputMode(args);
|
|
388
|
-
await resolveAuthentication().then((authentication) => authentication._tag === "
|
|
373
|
+
await resolveAuthentication().then((authentication) => authentication._tag === "Hosted" ? revokeHostedSession(authentication) : void 0).catch((error) => {
|
|
389
374
|
const message = error instanceof Error ? error.message : String(error);
|
|
390
|
-
logger.warn(`
|
|
375
|
+
logger.warn(`Hosted session revocation failed (${message}). Saved credentials are still cleared.`);
|
|
391
376
|
});
|
|
392
377
|
await clearTokens();
|
|
393
378
|
await clearBingCredentials();
|
|
@@ -446,4 +431,4 @@ const authCommand = defineCommand({
|
|
|
446
431
|
await runStatus(args);
|
|
447
432
|
}
|
|
448
433
|
});
|
|
449
|
-
export {
|
|
434
|
+
export { HOSTED_COMMANDS, authCommand, loginCommand, loginHosted, logoutCommand, statusCommand };
|
package/dist/commands/bing.mjs
CHANGED
|
@@ -56,8 +56,8 @@ const bingCommand = defineCommand({
|
|
|
56
56
|
const { json } = applyOutputMode(args);
|
|
57
57
|
const env = useCliRuntime().environment;
|
|
58
58
|
const authentication = await resolveAuthentication();
|
|
59
|
-
if (authentication._tag === "
|
|
60
|
-
if (args.oauth || args["api-key"] || args["client-id"] || args["client-secret"] || args["redirect-uri"]) throw new Error("Local Bing credentials
|
|
59
|
+
if (authentication._tag === "Hosted") {
|
|
60
|
+
if (args.oauth || args["api-key"] || args["client-id"] || args["client-secret"] || args["redirect-uri"]) throw new Error("Local Bing credentials need Local mode. Pass --mode local. Hosted mode uses the Bing connection saved on gscdump.com.");
|
|
61
61
|
if (!args.site) throw new Error("Choose --site SITE_ID from `gscdump bing sites` to connect Bing.");
|
|
62
62
|
const [site] = await resolveHostedBingSites(authentication, {
|
|
63
63
|
site: args.site,
|
|
@@ -128,7 +128,7 @@ const bingCommand = defineCommand({
|
|
|
128
128
|
args: OUTPUT_ARGS,
|
|
129
129
|
async run({ args }) {
|
|
130
130
|
const { json } = applyOutputMode(args);
|
|
131
|
-
if ((await resolveAuthentication())._tag === "
|
|
131
|
+
if ((await resolveAuthentication())._tag === "Hosted") throw new Error("Hosted authentication is shared with Google. Use `gscdump auth logout` to remove it.");
|
|
132
132
|
await clearBingCredentials();
|
|
133
133
|
const env = useCliRuntime().environment;
|
|
134
134
|
const environmentCredentials = Boolean(env.BING_API_KEY?.trim() || env.BING_ACCESS_TOKEN?.trim());
|
|
@@ -152,14 +152,14 @@ const bingCommand = defineCommand({
|
|
|
152
152
|
async run({ args }) {
|
|
153
153
|
const { json } = applyOutputMode(args);
|
|
154
154
|
const authentication = await resolveAuthentication();
|
|
155
|
-
if (authentication._tag === "
|
|
155
|
+
if (authentication._tag === "Hosted") {
|
|
156
156
|
const sites = args.site ? await resolveHostedBingSites(authentication, {
|
|
157
157
|
site: args.site,
|
|
158
158
|
requireConnected: false
|
|
159
159
|
}) : await listHostedBingSites(authentication);
|
|
160
160
|
console.log(JSON.stringify({
|
|
161
161
|
searchEngine: "bing",
|
|
162
|
-
mode: "
|
|
162
|
+
mode: "hosted",
|
|
163
163
|
sites
|
|
164
164
|
}, null, 2));
|
|
165
165
|
return;
|
|
@@ -183,7 +183,7 @@ const bingCommand = defineCommand({
|
|
|
183
183
|
async run({ args }) {
|
|
184
184
|
applyOutputMode(args);
|
|
185
185
|
const authentication = await resolveAuthentication();
|
|
186
|
-
if (authentication._tag === "
|
|
186
|
+
if (authentication._tag === "Hosted") {
|
|
187
187
|
console.log(JSON.stringify({
|
|
188
188
|
searchEngine: "bing",
|
|
189
189
|
sites: await listHostedBingSites(authentication)
|
|
@@ -223,7 +223,7 @@ const bingCommand = defineCommand({
|
|
|
223
223
|
},
|
|
224
224
|
"datasets": {
|
|
225
225
|
type: "string",
|
|
226
|
-
description: `Comma-separated datasets: ${BING_DATASETS.join(", ")}. Default: all. Crawl issues have no dates, so --start, --end, and
|
|
226
|
+
description: `Comma-separated datasets: ${BING_DATASETS.join(", ")}. Default: all. Crawl issues have no dates, so --start, --end, and Hosted mode leave them out`
|
|
227
227
|
},
|
|
228
228
|
"start": {
|
|
229
229
|
type: "string",
|
|
@@ -239,7 +239,7 @@ const bingCommand = defineCommand({
|
|
|
239
239
|
const { json } = applyOutputMode(args);
|
|
240
240
|
const options = parseBingDumpOptions(args);
|
|
241
241
|
const authentication = await resolveAuthentication();
|
|
242
|
-
if (authentication._tag === "
|
|
242
|
+
if (authentication._tag === "Hosted") {
|
|
243
243
|
if (options.datasetsExplicit && options.datasets.includes("crawl-issues")) throw new Error("Bing crawl issues require local authentication. Use --mode local.");
|
|
244
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
245
|
const sites = await resolveHostedBingSites(authentication, {
|
|
@@ -297,7 +297,7 @@ const bingCommand = defineCommand({
|
|
|
297
297
|
const url = URL.parse(args.url);
|
|
298
298
|
if (!url || !["http:", "https:"].includes(url.protocol)) throw new Error("Use a full HTTP or HTTPS page URL.");
|
|
299
299
|
const authentication = await resolveAuthentication();
|
|
300
|
-
if (authentication._tag === "
|
|
300
|
+
if (authentication._tag === "Hosted") {
|
|
301
301
|
const [site] = await resolveHostedBingSites(authentication, { site: args.site });
|
|
302
302
|
console.log(JSON.stringify(await inspectHostedBingUrl(authentication, site.siteId, args.url), null, 2));
|
|
303
303
|
return;
|
|
@@ -323,7 +323,7 @@ const bingCommand = defineCommand({
|
|
|
323
323
|
async run({ args }) {
|
|
324
324
|
applyOutputMode(args);
|
|
325
325
|
const authentication = await resolveAuthentication();
|
|
326
|
-
if (authentication._tag !== "
|
|
326
|
+
if (authentication._tag !== "Hosted") throw new Error("Hosted Bing connection verification needs Hosted mode. Pass --mode hosted.");
|
|
327
327
|
const [site] = await resolveHostedBingSites(authentication, {
|
|
328
328
|
site: args.site,
|
|
329
329
|
requireConnected: false
|
package/dist/commands/doctor.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { doctorCommandMeta } from "../command-meta.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { getHostedAccount, resolveAuthentication } from "../auth-state.mjs";
|
|
3
3
|
import { loadConfig } from "../config.mjs";
|
|
4
4
|
import { resolveCliEnvironment } from "../environment.mjs";
|
|
5
5
|
import { parseEnvFile } from "../env-file.mjs";
|
|
6
6
|
import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs";
|
|
7
|
-
import { getAuth, loadTokens, resolveAuth, resolveBYOK } from "../auth.mjs";
|
|
7
|
+
import { getAuth, isStaleServiceAccountPointer, loadTokens, resolveAuth, resolveBYOK, resolveServiceAccount } from "../auth.mjs";
|
|
8
8
|
import { createLocalStore } from "../local-store.mjs";
|
|
9
9
|
import { createCommandContext, formatSiteResolution } from "../context.mjs";
|
|
10
10
|
import { missingRequiredScopes } from "../auth-scopes.mjs";
|
|
@@ -76,17 +76,62 @@ function describeAuthSource(envKeys, byok) {
|
|
|
76
76
|
const isAccessToken = typeof byok === "string";
|
|
77
77
|
const driver = isAccessToken ? "GSC_ACCESS_TOKEN" : "GSC_REFRESH_TOKEN";
|
|
78
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 `
|
|
79
|
+
return `environment credentials ${isAccessToken ? "(access-token)" : "(refresh-token)"} from ${source} via ${driver}`;
|
|
80
80
|
}
|
|
81
81
|
async function checkAuth(envKeys) {
|
|
82
82
|
const checks = [];
|
|
83
|
+
const serviceAccount = await resolveServiceAccount().catch((error) => {
|
|
84
|
+
if (isStaleServiceAccountPointer(error)) {
|
|
85
|
+
checks.push({
|
|
86
|
+
name: "auth.service_account",
|
|
87
|
+
status: "warn",
|
|
88
|
+
detail: `ignored: ${error instanceof Error ? error.message : String(error)}`
|
|
89
|
+
});
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
93
|
+
});
|
|
94
|
+
if (serviceAccount instanceof Error) {
|
|
95
|
+
checks.push({
|
|
96
|
+
name: "auth",
|
|
97
|
+
status: "fail",
|
|
98
|
+
detail: `service account: ${serviceAccount.message}`
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
checks,
|
|
102
|
+
liveToken: null
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (serviceAccount) {
|
|
106
|
+
const token = await serviceAccount.getAccessToken().then((result) => result.token ?? null, (error) => error instanceof Error ? error : new Error(String(error)));
|
|
107
|
+
if (!token || token instanceof Error) {
|
|
108
|
+
checks.push({
|
|
109
|
+
name: "auth",
|
|
110
|
+
status: "fail",
|
|
111
|
+
detail: `service account ${serviceAccount.email}: ${token instanceof Error ? redactTokens(token.message) : "no access token"}`
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
checks,
|
|
115
|
+
liveToken: null
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
checks.push({
|
|
119
|
+
name: "auth",
|
|
120
|
+
status: "pass",
|
|
121
|
+
detail: `service account ${serviceAccount.email}`
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
checks,
|
|
125
|
+
liveToken: token
|
|
126
|
+
};
|
|
127
|
+
}
|
|
83
128
|
const byok = resolveBYOK();
|
|
84
129
|
const tokens = await loadTokens();
|
|
85
130
|
if (!byok && !tokens) {
|
|
86
131
|
checks.push({
|
|
87
132
|
name: "auth",
|
|
88
133
|
status: "fail",
|
|
89
|
-
detail: "no
|
|
134
|
+
detail: "no Google credentials: no service account, no environment tokens, no saved tokens. Run `gscdump init`, or `gscdump auth login --mode hosted` for Hosted mode"
|
|
90
135
|
});
|
|
91
136
|
return {
|
|
92
137
|
checks,
|
|
@@ -140,7 +185,7 @@ async function checkAuth(envKeys) {
|
|
|
140
185
|
detail: info.email
|
|
141
186
|
});
|
|
142
187
|
const scopes = info.scope ? info.scope.split(/\s+/) : [];
|
|
143
|
-
const missing = missingRequiredScopes(scopes
|
|
188
|
+
const missing = missingRequiredScopes(scopes);
|
|
144
189
|
if (missing.length > 0) checks.push({
|
|
145
190
|
name: "auth.scopes",
|
|
146
191
|
status: "warn",
|
|
@@ -324,8 +369,8 @@ function describeGscSites(sites, defaultSite) {
|
|
|
324
369
|
}
|
|
325
370
|
return checks;
|
|
326
371
|
}
|
|
327
|
-
async function
|
|
328
|
-
const account = await
|
|
372
|
+
async function checkHostedConnection(authentication) {
|
|
373
|
+
const account = await getHostedAccount(authentication).then((value) => ({
|
|
329
374
|
_tag: "Ok",
|
|
330
375
|
value
|
|
331
376
|
}), (error) => ({
|
|
@@ -341,16 +386,21 @@ async function checkCloudConnection(authentication) {
|
|
|
341
386
|
status: "warn",
|
|
342
387
|
detail: "skipped (auth failed)"
|
|
343
388
|
}];
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
389
|
+
const count = account.value.sites.length;
|
|
390
|
+
const sitesChecks = [count > 0 ? {
|
|
391
|
+
name: "hosted.sites",
|
|
392
|
+
status: "pass",
|
|
393
|
+
detail: `${count} hosted Site(s)`
|
|
394
|
+
} : {
|
|
395
|
+
name: "hosted.sites",
|
|
396
|
+
status: "warn",
|
|
397
|
+
detail: "no hosted Sites. Connect a Site at https://gscdump.com/app/onboarding?step=connect-sites"
|
|
398
|
+
}];
|
|
349
399
|
return [
|
|
350
400
|
{
|
|
351
401
|
name: "auth",
|
|
352
402
|
status: "pass",
|
|
353
|
-
detail: `
|
|
403
|
+
detail: `Hosted mode via ${authentication.apiRoot}`
|
|
354
404
|
},
|
|
355
405
|
{
|
|
356
406
|
name: "auth.account",
|
|
@@ -391,7 +441,7 @@ const doctorCommand = defineCommand({
|
|
|
391
441
|
const authentication = await resolveAuthentication();
|
|
392
442
|
const envResult = await checkEnv();
|
|
393
443
|
const [connectionChecks, dataDirChecks, watermarkChecks] = await Promise.all([
|
|
394
|
-
authentication._tag === "
|
|
444
|
+
authentication._tag === "Hosted" ? checkHostedConnection(authentication) : checkLocalConnection(envResult.envKeys),
|
|
395
445
|
checkDataDir(dataDir),
|
|
396
446
|
checkStoreWatermarks(dataDir)
|
|
397
447
|
]);
|
package/dist/commands/init.mjs
CHANGED
|
@@ -4,8 +4,8 @@ import { saveAuthentication } from "../auth-state.mjs";
|
|
|
4
4
|
import { defaultDataDir, loadConfig, saveConfig } from "../config.mjs";
|
|
5
5
|
import { applyCliEnvironment } from "../environment.mjs";
|
|
6
6
|
import { OUTPUT_ARGS, applyOutputMode, displayPath, logger } from "../utils.mjs";
|
|
7
|
-
import { authenticate, getAuth, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
|
|
8
|
-
import {
|
|
7
|
+
import { ACCESS_NOT_SET_UP, authenticate, getAuth, loadTokens, resolveBYOK, resolveServiceAccount, saveTokens } from "../auth.mjs";
|
|
8
|
+
import { loginHosted } from "./auth.mjs";
|
|
9
9
|
import process from "node:process";
|
|
10
10
|
import { defineCommand } from "citty";
|
|
11
11
|
import fs from "node:fs/promises";
|
|
@@ -61,15 +61,15 @@ const initCommand = defineCommand({
|
|
|
61
61
|
},
|
|
62
62
|
"mode": {
|
|
63
63
|
type: "string",
|
|
64
|
-
description: "
|
|
64
|
+
description: "Access mode to save: local or hosted"
|
|
65
65
|
},
|
|
66
66
|
"api-key": {
|
|
67
67
|
type: "string",
|
|
68
|
-
description: "gscdump user API key for --mode
|
|
68
|
+
description: "gscdump user API key for --mode hosted; defaults to GSCDUMP_API_KEY"
|
|
69
69
|
},
|
|
70
70
|
"api-root": {
|
|
71
71
|
type: "string",
|
|
72
|
-
description: "
|
|
72
|
+
description: "Hosted API root for --mode hosted; defaults to GSCDUMP_API_ROOT or https://gscdump.com/api"
|
|
73
73
|
},
|
|
74
74
|
...OUTPUT_ARGS
|
|
75
75
|
},
|
|
@@ -77,9 +77,9 @@ const initCommand = defineCommand({
|
|
|
77
77
|
applyOutputMode(args);
|
|
78
78
|
const config = await loadConfig();
|
|
79
79
|
const mode = useCliRuntime().authModeOverride;
|
|
80
|
-
if (mode === "
|
|
81
|
-
await
|
|
82
|
-
|
|
80
|
+
if (mode === "hosted") {
|
|
81
|
+
await loginHosted(args);
|
|
82
|
+
printHostedNextSteps();
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
85
|
if (config.clientId && config.clientSecret && !args.force) {
|
|
@@ -104,11 +104,23 @@ const initCommand = defineCommand({
|
|
|
104
104
|
...dataDir ? { dataDir } : {}
|
|
105
105
|
});
|
|
106
106
|
await saveAuthentication({ _tag: "Local" });
|
|
107
|
-
logger.success(`
|
|
107
|
+
logger.success(`Google credentials found in the environment (${typeof byok === "string" ? "access-token" : "refresh-token"}). Login skipped.`);
|
|
108
108
|
logger.success("Setup complete.");
|
|
109
109
|
printNextSteps();
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
|
+
const serviceAccount = await resolveServiceAccount().catch(() => null);
|
|
113
|
+
if (serviceAccount) {
|
|
114
|
+
const dataDir = args.store ? config.dataDir ?? defaultDataDir() : config.dataDir;
|
|
115
|
+
await saveConfig({
|
|
116
|
+
...config,
|
|
117
|
+
...dataDir ? { dataDir } : {}
|
|
118
|
+
});
|
|
119
|
+
await saveAuthentication({ _tag: "Local" });
|
|
120
|
+
logger.success(`Service account found (${serviceAccount.email ?? "key file"}). Login skipped.`);
|
|
121
|
+
printNextSteps();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
112
124
|
const envFile = await loadEnvFile();
|
|
113
125
|
const envCid = envFile?.GSC_CLIENT_ID ?? envFile?.GOOGLE_CLIENT_ID;
|
|
114
126
|
const envSec = envFile?.GSC_CLIENT_SECRET ?? envFile?.GOOGLE_CLIENT_SECRET;
|
|
@@ -145,11 +157,7 @@ const initCommand = defineCommand({
|
|
|
145
157
|
}
|
|
146
158
|
if (!canPrompt()) {
|
|
147
159
|
if (!await loadTokens()) {
|
|
148
|
-
logger.error(
|
|
149
|
-
"No Google credentials found. Init cannot prompt without a terminal.",
|
|
150
|
-
"Run `gscdump auth login` in a terminal, or set GSC_CLIENT_ID, GSC_CLIENT_SECRET and GSC_REFRESH_TOKEN.",
|
|
151
|
-
"To use gscdump.com, run `gscdump auth login --mode cloud`."
|
|
152
|
-
].join("\n"));
|
|
160
|
+
logger.error(`Init cannot prompt without a terminal.\n${ACCESS_NOT_SET_UP}`);
|
|
153
161
|
process.exit(1);
|
|
154
162
|
}
|
|
155
163
|
await saveConfig({
|
|
@@ -188,6 +196,14 @@ function printNextSteps(sites = []) {
|
|
|
188
196
|
line(`gscdump sync --status --site ${site}`, "see what the Store holds");
|
|
189
197
|
console.log();
|
|
190
198
|
}
|
|
199
|
+
function printHostedNextSteps() {
|
|
200
|
+
console.log();
|
|
201
|
+
console.log(" Next:");
|
|
202
|
+
const line = (command, note) => console.log(` ${command.padEnd(44)} # ${note}`);
|
|
203
|
+
line("gscdump sites", "list your hosted Sites and their sync state");
|
|
204
|
+
line("gscdump query --site <site> -d query", "read the hosted record");
|
|
205
|
+
console.log();
|
|
206
|
+
}
|
|
191
207
|
async function runSmokeTest(oauth) {
|
|
192
208
|
const sites = await googleSearchConsole(oauth).sites().catch((e) => e);
|
|
193
209
|
if (sites instanceof Error) {
|
|
@@ -241,4 +257,4 @@ async function maybeWriteEnvFile(clientId, clientSecret) {
|
|
|
241
257
|
await fs.writeFile(envPath, content, { mode: 384 });
|
|
242
258
|
logger.success(`Wrote ${displayPath(envPath)}`);
|
|
243
259
|
}
|
|
244
|
-
export { initCommand, printNextSteps, runSmokeTest };
|
|
260
|
+
export { initCommand, printHostedNextSteps, printNextSteps, runSmokeTest };
|